Feedback regarding default equals, hashCode and toString implementions for records
Jan Boerman
janboerman95 at gmail.com
Mon Apr 27 14:20:40 UTC 2020
Hi all,
While experimenting with the new records, I found what I believe is an
oversight in the implementation.
The generated equals method for records is based on equality of the
components, but when the record component is an array, it still uses
regular Object#equals(Object) method.
This is not what Java developers would normally write (or generate using
IDEs) for their data carrier classes, as instead they would use
Arrays.equals or Arrays.deepEquals.
I believe that in order for records to succeed, they need to be truly
boilerplate-reducing, so I'm suggesting the following changes to the
default equals, hashCode and toString implementations:
IF the field in the record is statically known to be a one-dimensional
array:
THEN java.util.Arrays.equals(array1, array2) is used to compare the
component of two record instances (applies to
<recordclass>#equals(Object))
AND java.util.Arrays.hashCode(array) is used to compute the hashcode of
the component (applies to <recordclass>#hashCode())
AND java.util.Arrays.toString(array) is used to compute the string
representation of the component (applies to
<recordclass>#toString())
ELSE IF the field in the record is statically known to be a
multidimensional array:
THEN java.util.Arrays.deepEquals(array1, array2) is used to compare the
component of two record instances (applies to
<recordclass>#equals(Object))
AND java.util.Arrays.deepHashCode(array) is used to compute the hashcode
of the component (applies to <recordclass>#hashCode())
AND java.util.Arrays.deepToString(array) is used to compute the string
representation of the component (applies to
<recordclass>#toString())
ELSE
just use the old code generation algorithm from Java 14 that uses
java.lang.Object#equals
I am not familiar with the compiler implementation details, but it looks
like a trivial change to me.
There is a cost to compilation speed for doing these extra checks, so some
performance testing would be required.
One might argue that therefore developers shouldn't use arrays for record
components, but a data carrier class with a constructor with a varargs
parameter is common-enough scenario that I think this should be supported
properly.
The example code (written by me, public domain licensed) below demonstrates
the issue. It is also available here in this gist:
https://gist.github.com/Jannyboy11/067f5966afc6130711b2035795a45ea5
I did not include anything related to hashCode, but one can easily infer
what the issue there would be.
Kind regards,
Jan Boerman
```java
public class RecordTest {
private static record Messages(String... messages) {
public void println() {
System.out.println("Messages[messages=" +
Arrays.toString(messages) + "]");
}
}
private static record JaggedArrayRecord(String[][] world) {
public void println() {
System.out.println("JaggedArrayRecord[world=" +
Arrays.deepToString(world) + "]");
}
}
public static void main(String[] args) {
//single array example
System.out.println();
System.out.println("--- Example using a record with a field that is
a one dimensional array ---");
System.out.println();
String[] m1 = new String[] { new String("Hello") };
String[] m2 = new String[] { new String("Hello") };
Messages messages1 = new Messages(m1);
Messages messages2 = new Messages(m2);
messages1.println();
System.out.println(messages1); //expecting the same output as the
output from messages1.println()
boolean arraysEquals = Arrays.equals(m1, m2);
boolean objectEquals = messages1.equals(messages2); //expecting the
same boolean value as Arrays.equals(m1, m2)
System.out.println("equal according to
Object#equals(Object) ?: " + objectEquals);
System.out.println("equal according to Arrays#equals(Object[],
Object[]) ?: " + arraysEquals);
if (arraysEquals != objectEquals) {
System.out.println("These equality implementations don't yield
the same value, which is not desirable");
} else {
System.out.println("These equality implementations yield the
same value, which is desirable");
}
//jagged array example
System.out.println();
System.out.println("--- Example using a record with a field that is
a multidimensional array ---");
System.out.println();
String[][] world1 = new String[][] { new String[] { new
String("World!") } };
String[][] world2 = new String[][] { new String[] { new
String("World!") } };
JaggedArrayRecord jagged1 = new JaggedArrayRecord(world1);
JaggedArrayRecord jagged2 = new JaggedArrayRecord(world2);
jagged1.println();
System.out.println(jagged1); //expecting the same output as the
output from jar1.println()
boolean jaggedArraysDeepEquals = Arrays.deepEquals(world1, world2);
boolean jaggedObjectEquals = jagged1.equals(jagged2);
//expecting the same boolean value as Arrays.deepEquals(world1, world2)
System.out.println("equal according to
Object#equals(Object) ?: " + jaggedObjectEquals);
System.out.println("equal according to Arrays#deepEquals(Object[],
Object[]) ?: " + jaggedArraysDeepEquals);
if (arraysEquals != objectEquals) {
System.out.println("These equality implementations don't yield
the same value, which is not desirable");
} else {
System.out.println("These equality implementations yield the
same value, which is desirable");
}
}
}
```
More information about the amber-dev
mailing list