Getters vs. public final fields in records

Brian Goetz brian.goetz at oracle.com
Wed Aug 10 17:32:59 UTC 2022


So, I want to make sure this conversation stays on track.  You asked 
"why does it work this way", and you got an answer.  And its OK to ask 
follow-up questions for clarification.  But we need to steer away from 
the (irresistibly tempting, I know) track of "let's redesign the 
feature", and we're in danger of veering into that. These issues were 
well considered during the design already, and these questions are not 
new.  As mentioned, we did explicitly consider public fields, and 
concluded that would be a bad idea.  (I know this may sound unfriendly, 
but just consider how this scales. There are 10M Java developers, who 
may each independently have their own ideas about how a feature should 
be designed, and want to "debate" it, sequentially and independently and 
possibly inconsistently, with the language designers.)

Now, to your question.

> If we look at what record-like things look like in other languages and 
> settings (C structs, ML records, Pascal records) accessors are the 
> strange choice that needs justification.
>
>     In any case, the answer is simple: not all objects are immutable.
>     If a
>     record component were an array, or an ArrayList, or any other object
>     with mutable state, having public fields would make it impractical to
>     use these types in records in many cases, because we'd be unable to
>     expose their state without also exposing their mutability.
>
>
> I think you are suggesting techniques like explicitly defining 
> accessors for mutable record components that make defensive copies? 
> Are we really comfortable calling records “a simple aggregation of 
> values” if I have to read the documentation or implementation of an 
> accessor just to understand what the following code does:
>

This is exactly as we intended it to work.  For records whose components 
are values (e.g., `record Point(int x, int y) {}`), the default 
implementation of constructor and accessor do exactly the right thing, 
and no one has to write any code.  But when mutability rears its head, 
the "right thing" is less obvious, and making a one-size-doesn't-fit-all 
decision will make some users unhappy.

As it turns out, the mathematical construct from Domain Theory known as 
"embedding-projection pairs" (which is a formalization of approximation) 
offer us a useful way to talk about the right thing. The key invariant 
that records must adhere to (specified in the refined contract of 
`Record::equals`) is that unpacking a record components, and repacking 
in a new record, should yield an "equals" record:

     r.equals(new R(r.c0(), r.c1(), ..., r.cn())

(This is as close as we can say in Java to "construction and 
deconstruction form an embedding-projection pair between the space of 
records and the cartesian product space of their components.")

The reason that the situation you describe is inevitable comes from the 
fact that for mutable components (such as arrays), in some cases we want 
to judge two records equal if they hold the same _array object_, and in 
other cases we want to judge two records equal if they hold arrays _with 
the same contents_.  And the language doesn't know which you want -- and 
it requires very little imagination to construct cases where each of 
these interpretations will be wrong. So unless we're willing to outlaw 
records that are not immutable all the way down, which would make 
records much less useful, we have to give people a way to say what they 
want.  This is just like how we let map classes decide whether equals() 
means "same map", or means "same mappings".  Both are valid 
interpretations, and both should be expressible.  Similarly, some 
records may want to expose the mutability of their components to 
clients, and others will want to launder using defensive copies.  All of 
these are expressible in the record model, just with different degrees 
of explicit code.

If your component already chooses the answer for equals that you want -- 
such as ArrayList::equals comparing lists by contents, or arrays 
comparing by identity -- then you can do nothing.  Otherwise, you have 
to override the constructor, accessor, and equals in concert to preserve 
the invariant that deconstruction and reconstruction yields something 
equivalent to the original.

So if you define a record whose components are mutable, or for whom you 
don't want the record equivalence semantics to be the same as the 
component equals, you're going to have to write some code -- but more 
importantly, you're going to have to tell your users what equality means 
for *your* record.  Just like you're supposed to specify what equality 
means for every class.  It might be tempting to say "records are just 
structural tuples, so there's nothing interesting to say about 
equality", but that turns out to be wishful thinking. Consistent with 
the choice we've made elsewhere (functional interfaces are nominal 
function types, not structural ones; sealed types are nominal union 
types, not structural ones), the rational choice for Java's product 
types is also nominal, not structural.

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20220810/1f9bad80/attachment.htm>


More information about the amber-dev mailing list