Records feedback

Brian Goetz brian.goetz at oracle.com
Fri Nov 22 14:05:27 UTC 2019


Thanks for the feedback.  Let me add some clarifications for the record.  

> They will clearly have their uses, but those
> uses will not be many of the cases where beans are currently used.

This comes as no surprise to us.  As we described nearly two years ago in the first version of this document: 

    http://cr.openjdk.java.net/~briangoetz/amber/datum.html <http://cr.openjdk.java.net/~briangoetz/amber/datum.html>

One of the central challenges of a feature like this is that there are multiple credible candidates for the design center, which is why we explicitly set out a set of competing perspectives, with cute names.  In the end, JavaBean Jerry was not the winner; Tuples Tommy was.  And this choice of design center affects nearly every aspect of the final feature design.  

The outcome is that *records are nominal tuples*, so we would expect them to work best where we might use tuples (data carriers across boundaries, compound map keys, etc), and not as well where we use beans.  Beans-lovers may well be disappointed — but they probably would have been disappointed with a beans-centric version of this feature anyway.  On the other hand, you may find that they have more uses than you initially think.  

> - I suspect that the ability to override equals/hashCode/toString will
> lead to subtle puzzlers/bugs. I'm not sure there is a viable
> alternative however, as there are bound to be some valid use cases for
> overriding the defaults.

Indeed, this is an area of compromise.  Given that the language has arrays, which are immutability-hostile, it is really not credible to prevent accessors from being explicitly specified.  (There are other cases where this is warranted, but arrays are the 80% case here.)  And once you open this door, you can indeed get to subtle inconsistencies.  The invariant we settled on, as specified in java.lang.Record, is that constructors/accessors/equals must be overridden consistently, to achieve the following invariant: if R is a record with components c1..cn, then:

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

In other words, “copying” the record via the API must result in an .equals() record.  This is less strict than the invariants we initially hoped for (see early versions of the documents), but seems a pragmatic compromise that developers can learn to follow.  Of course, just like consistency requirement between equals and hashCode, saying it in the spec doesn’t mean people will do it.  

> - The compact constructor looks to be a reasonable solution to adding
> validation. However, it is not declarative thus cannot be queried
> using framework code - something that is useful when building a
> serialization framework.

I think this is a misunderstanding.  The compact constructor is merely sugar for the canonical constructor — the one whose argument list matches the record signature.  The record always has such a canonical constructor, whether specified implicitly, compactly, or in full glory — and frameworks can count on it, and need not reason about how it got there.  I think this is the outcome you want.  

> - I find the requirement to have a public constructor undesirable,
> although I understand the rationale. Having spent years avoiding
> constructors, I don't want to go back to writing `new` everywhere.
> This may well need "factory as a language feature", but such as thing
> would surely be a good thing. Even allowing Foo.new() would be a step
> forward.

Yes, we spent a fair amount of time struggling with this.  I agree that “new” has the look of “old” libraries.  And we did explore whether it was practical to do something about factories in the language (including what you suggest, effectively allowing “new” as a name of a static factory method.)  In the end, we decided it wasn’t worth it.  Effective Java lists the following advantages of factories over constructors: 

 - Can have informative names / avoid subtle override tricks
 - Can return an instance of a subtype
 - Can return a cached instance
 - Can take advantage of type inference for generic type parameters

Because records are defined as such a simple abstraction, most of these benefits are not relevant.  Informative names are arguably valuable here, but if records had a mandated factory, the name would not be all that informative anyway.  Records are final, so returning an instance of refined type is not relevant.  Caching is unlikely to be all that important to many records (but if it is, you can easily write your own caching factory.)  And since Java 7, the asymmetry regarding type inference is no longer a consideration.  

So, the sole remaining argument against — which I have a lot of sympathy for! — is “it looks like an old library.”  The cost of fixing this, however, would have been either delaying records or rushing a static factory feature that was otherwise not anywhere near the top of the priority list, so this seems like a pragmatic compromise.  

> My biggest complaint however is the syntax

The great thing about syntax is it affords so many opinions!  But, there’s a reason why what you propose (which actually was discussed) does not meet the requirements, and why we chose what we did.  

Going back to the design center, records are _nominal tuples_.  Since a record _is_ its state, the state description should be front and center.  (This should be enough, but there’s more.)

All of the relevant protocols — construction, deconstruction (when we have deconstruction patterns), equality, and hashing, are derived from the state description.  The latter two are order independent, but the first two are order dependent — clients will call constructors with positional arguments, and similarly for deconstruction patterns when we have them.  There needs to be some declaration where the order of components is clearly specified; inferring it from the order in which the fields happen to be declared is way too brittle.  (We have 25 years of experience telling us that reordering field declarations is a no-op; for this to all of a sudden be a source- and binary- incompatible change would be playing a mean trick on the users.)  So the knock on this is not verbosity, but clarity and robustness.  

There is room to extend the record syntax to enums, when they have fields and constructors:

    enum Color(int rgb) {
        RED(0xFF0000), BLUE(0x0000FF);
    }

where the enum automatically acquires fields and constructors, record-style.  One could even consider applying this to classes, but we have deliberately avoided doing so until we have the whole concision story for classes in our sights, lest we paint ourselves into a corner.  

Where your analogies with enum declaration syntax are more likely to be fruitful is with sealed types; sums of products are expected to be a common thing, and are a straightforward generalization of enums.  Indeed, there have been discussions along these lines on the EG list.

Cheers,
-Brian




More information about the amber-dev mailing list