Records (preview): Why aren't accessor methods generated as final?
Brian Goetz
brian.goetz at oracle.com
Fri Apr 24 21:36:14 UTC 2020
We tried it both ways; both have benefits and flaws, and we tried to
pick the lesser of evils.
Since records are primarily a semantic mechanism, we started with
invariants we want to enforce. After searching around for the right
invariants, we landed on this one (which is mandated by the refined
contract of `Record::equals`):
If
R r = ...
R rr = new R(r.a(), r.b(), ... r.z());
then
r.equals(rr)
This is consist with "a record _is_ its state".
Now, consider the existence of mutable objects, that may only support
identity equality. Arrays are an obvious and particularly irritating
example (as they are part of the language and we can't override either
of these characteristics), but there are others. We might want to:
- Use defensive copies in the constructor and/or the accessors to keep
the contents from getting polluted;
- Override equals() to use content-equality rather than identity equality.
Neither of these are _forced_ moves; it is totally reasonable to want to
use an array component as is here, if the semantics of your record is
that two "array boxes" are the same if they hold the same array
instance. But this is not the only thing people might want if they put
an array component in a record.
Note that
record R(int[] array) { }
meets the invariant out of the gate. But if we want to do a defensive
copy in the constructor:
record R(int[] array) {
R { array = array.clone(); }
}
it no longer does; the changed identity will cause the two records to
compare as unequal. But we can fix this by overriding equals (which is
why equals and hashCode are not final):
record R(int[] array) {
R { array = array.clone(); }
boolean equals(Object o) {
return o instanceof R r && Arrays.equals(array, r.array);
}
}
Now, you might observe that while this protects the array from aliasing
on the way in, it doesn't do so on the way out, and this is an entirely
sensible thing to want to do in an accessor:
record R(int[] array) {
R { array = array.clone(); }
int[] array() { return array.clone(); }
boolean equals(Object o) {
return o instanceof R r && Arrays.equals(array, r.array);
}
}
and so we have to admit overriding accessors too.
Obviously, these degrees of freedom -- overriding accessors or equals --
can be abused. And such abuse will cause them to be not compliant with
the refined specification of java.lang.Record. Will people care? Well,
we know the answer: bad programmers will move heaven and earth to get it
wrong.
Also, it seems likely that these "escape hatches" will be used far more
frequently than one might hope they are.
The reality is that this is a complex tradeoff where all the choices are
less than ideal. We went pretty hard in the "not very much flexibility"
direction, enough so that people complain about that, but there's also
room to complain about "too much flexibility."
On 4/24/2020 5:04 PM, Mike Bishop wrote:
> I'm curious as to why the accessor methods in a record are not final.
> Consider the following record declaration:
>
> public record Point(double x, double y) {}
>
> The accessor methods x() and y() return x and y, respectively, but are not
> final. This allows me to override x(), for example, as follows:
>
> public record Point(double x, double y) {
> public double x() {
> return x * x;
> }
> }
>
> I can test the above Point record with the following code to ensure that
> two Points with the same declared x and y coordinates are equal.
>
> public static void main(String[] args) {
> var p = new Point(3.0, 4.0);
> var pPrime = new Point(p.x(), p.y());
> System.out.println("p = " + p + ", pPrime = " + pPrime);
> assert pPrime.equals(p);
> }
>
> As expected, the assertion fails as pPrime is (9.0, 4.0) while p is (3.0,
> 4.0).
>
> My concern is that if I'm using a Point in my code, I would expect that two
> Points with the same x and y will be equivalent since the 2-tuple (x, y)
> represents "the state, the whole state and nothing but the state" of Point.
> I think this can only be assured if the accessor methods are final.
>
> Best regards,
> Mike Bishop
More information about the amber-dev
mailing list