21

Given the following code:

public static void main(String[] args) {
    record Foo(int[] ints){}

    var ints = new int[]{1, 2};
    var foo = new Foo(ints);
    System.out.println(foo); // Foo[ints=[I@6433a2]
    System.out.println(new Foo(new int[]{1,2}).equals(new Foo(new int[]{1,2}))); // false
    System.out.println(new Foo(ints).equals(new Foo(ints))); //true
    System.out.println(foo.equals(foo)); // true
}

It seems, obviously, that array's toString, equals methods are used (instead of static methods, Arrays::equals,Arrays::deepEquals or Array::toString).

So I guess Java 14 Records (JEP 359) don't work too well with arrays, the respective methods have to be generated with an IDE (which at least in IntelliJ, by default generates "useful" methods, i.e. they use the static methods in Arrays).

Or is there any other solution?

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
user140547
  • 7,750
  • 3
  • 28
  • 80
  • 3
    How about using `List` instead of an array? – Oleg Apr 16 '20 at 23:03
  • I don't understand why such methods have to be generated with an IDE? All methods should be able to be generated by hand. – NomadMaker Apr 16 '20 at 23:16
  • 1
    The `toString()`,`equals()` and `hashCode()` methods of a record are implemented [using an invokedynamic reference.](https://stackoverflow.com/a/61211332/1746118). If only the compiled class equivalent could have been closer to what the method `Arrays.deepToString` does in its private overloaded method today, it *might* have solved for the primitives cases. – Naman Apr 17 '20 at 02:00
  • 1
    To second the design choice for implementation, for something not providing an overridden implementation of these methods, it's not a bad idea to fall back to `Object`, since that could happen with user-defined classes as well. e.g [incorrect equals](https://stackoverflow.com/questions/60796961/) – Naman Apr 17 '20 at 02:06
  • 2
    @Naman The choice to use `invokedynamic` has absolutely nothing to do with the selection of semantics; indy is a pure implementation detail here. The compiler could have emitted bytecode to do the same thing; this was just a more efficient and flexible way to get there. It was extensively discussed during the design of records whether to use a more nuanced equality semantics (such as deep equality for arrays), but this turned out to cause way more problems than it supposedly solved. – Brian Goetz Apr 18 '20 at 15:37

3 Answers3

35

Java arrays pose several challenges for records, and these added a number of constraints to the design. Arrays are mutable, and their equality semantics (inherited from Object) is by identity, not contents.

The basic problem with your example is that you wish that equals() on arrays meant content equality, not reference equality. The (default) semantics for equals() for records is based on equality of the components; in your example, the two Foo records containing distinct arrays are different, and the record is behaving correctly. The problem is you just wish the equality comparison were different.

That said, you can declare a record with the semantics you want, it just takes more work, and you may feel like is is too much work. Here's a record that does what you want:

record Foo(String[] ss) {
    Foo { ss = ss.clone(); }
    String[] ss() { return ss.clone(); }
    public boolean equals(Object o) { 
        return o instanceof Foo f && Arrays.equals(f.ss, ss);
    }
    public int hashCode() { return Objects.hash(Arrays.hashCode(ss)); }
}

What this does is a defensive copy on the way in (in the constructor) and on the way out (in the accessor), as well as adjusting the equality semantics to use the contents of the array. This supports the invariant, required in the superclass java.lang.Record, that "taking apart a record into its components, and reconstructing the components into a new record, yields an equal record."

You might well say "but that's too much work, I wanted to use records so I didn't have to type all that stuff." But, records are not primarily a syntactic tool (though they are syntactically more pleasant), they are a semantic tool: records are nominal tuples. Most of the time, the compact syntax also yields the desired semantics, but if you want different semantics, you have to do some extra work.

Holger
  • 285,553
  • 42
  • 434
  • 765
Brian Goetz
  • 90,105
  • 23
  • 150
  • 161
  • 11
    Also, it is a common mistake by people who wish that array equality was by contents to assume that _no one ever_ wants array equality by reference. But that is simply not true; it's just that there is no one answer that works for all cases. Sometimes reference equality is exactly what you want. – Brian Goetz Apr 17 '20 at 19:23
  • 1
    Is ```Objects.hash()``` in ```Objects.hash(Arrays.hashCode(ss))``` necessary / useful? I guess ```Arrays.hashCode(ss)``` would suffice, right? – jcsahnwaldt Reinstate Monica Nov 19 '21 at 19:30
  • 4
    @jcsahnwaldtReinstateMonica Records usually have more than one component; it is sensible to compute a hash value for each component, and then combine those hash values (such as with `Objects.hash(Object...)`). It looks weird because there's only one component here. – Brian Goetz Nov 19 '21 at 21:56
12

List< Integer > workaround

Workaround: Use a List of Integer objects (List< Integer >) rather than array of primitives (int[]).

In this example, I instantiate an unmodifiable list of unspecified class by using the List.of feature added to Java 9. You could just as well use ArrayList, for a modifiable list backed by an array.

package work.basil.example;

import java.util.List;

public class RecordsDemo
{
    public static void main ( String[] args )
    {
        RecordsDemo app = new RecordsDemo();
        app.doIt();
    }

    private void doIt ( )
    {

        record Foo(List < Integer >integers)
        {
        }

        List< Integer > integers = List.of( 1 , 2 );
        var foo = new Foo( integers );

        System.out.println( foo ); // Foo[integers=[1, 2]]
        System.out.println( new Foo( List.of( 1 , 2 ) ).equals( new Foo( List.of( 1 , 2 ) ) ) ); // true
        System.out.println( new Foo( integers ).equals( new Foo( integers ) ) ); // true
        System.out.println( foo.equals( foo ) ); // true
    }
}
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • It's not always a good idea though to choose List over an array and we were [discussing this anyway.](https://stackoverflow.com/questions/61261226/java-14-records-and-arrays/61261537?noredirect=1#comment108378050_61261537) – Naman Apr 17 '20 at 02:10
  • 1
    @Naman I never made a claim about being a “good idea”. The Question literally asked for other solutions. I provided one. And I did so an hour before the comments you linked. – Basil Bourque Apr 17 '20 at 06:19
  • 1
    To ensure the record cannot be created with a mutable List, you should perform a `List.copyOf` inside the record constructor. `copyOf` will only create an immutable copy as necessary and is a no-op if the list is already immutable. – A248 Mar 14 '21 at 22:52
6

Workaround: Create an IntArray class and wrap the int[].

record Foo(IntArray ints) {
    public Foo(int... ints) { this(new IntArray(ints)); }
    public int[] getInts() { return this.ints.get(); }
}

Not perfect, because you now have to call foo.getInts() instead of foo.ints(), but everything else works the way you want.

public final class IntArray {
    private final int[] array;
    public IntArray(int[] array) {
        this.array = Objects.requireNonNull(array);
    }
    public int[] get() {
        return this.array;
    }
    @Override
    public int hashCode() {
        return Arrays.hashCode(this.array);
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null || getClass() != obj.getClass())
            return false;
        IntArray that = (IntArray) obj;
        return Arrays.equals(this.array, that.array);
    }
    @Override
    public String toString() {
        return Arrays.toString(this.array);
    }
}

Output

Foo[ints=[1, 2]]
true
true
true
Andreas
  • 154,647
  • 11
  • 152
  • 247
  • 1
    Isn't that equivalent to telling to use a class and not a record in such a case? – Naman Apr 17 '20 at 01:00
  • 1
    @Naman Not at all, because your `record` may consist of many fields, and only the array field(s) are wrapped like this. – Andreas Apr 17 '20 at 01:31
  • I get the point you're trying to make in terms of reusability, but then there are inbuilt classes such as `List` which provide that kind of wrapping one might seek with the solution as proposed here. Or do you consider that could be an overhead for such use cases? – Naman Apr 17 '20 at 01:40
  • 3
    @Naman If you want to store an `int[]`, then a `List` is not the same, for at least two reasons: 1) The list uses a lot more memory, and 2) There is no built-in conversion between them. `Integer[]` `List` is fairly easy (`toArray(...)` and `Arrays.asList(...)`), but `int[]` `List` takes more work, and the conversion takes time, so it's something you don't want to do all the time. If you have an `int[]` and want to store it in a record (with other stuff, otherwise why use a record), and need it as an `int[]`, then converting every time you need it is wrong. – Andreas Apr 17 '20 at 01:51
  • Good point indeed. I could though if you allow nitpicking asking what benefits do we still see with `Arrays.equals`, `Arrays.toString` over the `List` implementation used while replacing the `int[]` as suggested in the other answer. (Assuming both these are workarounds anyway.) – Naman Apr 17 '20 at 02:13