Looking beyond records: better constructors (and deconstructors)

Brian Goetz brian.goetz at oracle.com
Fri Jun 7 19:11:05 UTC 2019


With most of the decisions regarding records being settled, let's take a 
few minutes to look down the road.  Records are great for where they 
apply, but there are plenty of classes that suffer from error-prone 
boilerplate that do not qualify to be records. We would like for some of 
the record goodies to filter down to ordinary classes, where possible.

The lowest-hanging fruit here is constructors: many constructors look 
vaguely like (or can be made to look like) this:

     Foo(ARGS) {
         if (ARGS NOT VALID)
             throw new IllegalArgumentException(...);

         if (ARGS NEED TO BE NORMALIZED / COPIED) {
             ARGS = normalize(ARGS);
         }

         this.ARGS = ARGS;
     }

That is, many constructors take arguments that are candidate values for 
their fields, and then validate the arguments, possibly normalize or 
defensively copy them, and write them to the corresponding fields with 
the error-prone boilerplate of:

     this.x = x;
     this.y = y;

Similarly, when we add deconstruction patterns, a deconstructor will 
likely have the similar idiom, in reverse:

     x = this.x;
     y = this.y;

Records sidestep this because we have already committed to a 
deterministic relationship between the public 
construction/deconstruction protocol and the internal representation.  
And records let you skip the initialization boilerplate, even if you 
have an explicit constructor:

     record Range(int low, int high) {
         public Range {
             if (low > high)
                 throw new IAE("Bad range: [%d, %d]".formatted(low, high));
             // Implicit field initialization FTW!
         }
     }

The author provides the explicit validity check, but the compiler fills 
in the boilerplate field initialization.  Now, the constructor only 
contains the "non-obvious" code.  Can we share this with ordinary classes?

What we would need is to tell the compiler that the constructor argument 
"int low" and the field "int low" are describing the same thing.  This 
is commonly the case, but purely convention.

We could, through a variety of syntactic indicators, capture this 
relationship.  (Please, let's decide whether we like the feature before 
we bikeshed the syntax.)  For example:

     class Foo {
         private int x;

         public Foo(this int x) { }
     }

where `this int x` means that the constructor has a parameter `int x`, 
which corresponds to the field `int x` of the current class. The 
compiler can reciprocate by filling in the `this.x = x` boilerplate 
where needed, and the same with a deconstruction pattern:

     class Foo {
         private int x;

         public Foo(this int x) { }

         public pattern Foo(this int x) { }
     }

If the constructor wants to do validation and/or normalization, it is 
like what we do with records -- we put that in the constructor, mutating 
the arguments if needed, and then the argument values are committed to 
the fields implicitly if they are DU on all paths out of the ctor.

With such a feature, then the special constructor form for records:

         public Range { STUFF }

becomes simply shorthand for

         public Range(this int low, this int high) { STUFF }

reducing some of the "magic" associated with records.




More information about the amber-spec-experts mailing list