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?
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.
List< Integer >
workaroundWorkaround: 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
}
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With