Records: construction and validation

Brian Goetz brian.goetz at oracle.com
Mon Mar 12 17:48:35 UTC 2018


Here's a sketch of where our thinking is right now for construction and 
validation.

General goal: As Kevin pointed out, we should make adding incremental 
validation easy, otherwise people won't do it, and the result is worse 
code.  It should be simple to add validation (and possibly also 
normalization) logic to constructors without falling off the syntactic 
cliff, either in the declaration or the body of the constructor.

All records have a /default constructor/.  This is the one whose 
signature matches the class signature.  If you don't have an explicit 
one, you get an implicit one, regardless of whether or not there are 
other constructors.

If you have records:

     abstract record A(int a) { }
     record B(int a, int b) extends A(a) { }

then the behavior of the default constructor for B is:

     super(a);
     this.b = b;

If you want to provide an explicit constructor to ensure, for example, 
that b > 0, you could just say it yourself:

     public B(int a, int b) {
         if (b <= 0)
             throw new IllegalArgumentException("b");
         super(a);
         this.b = b;
     }

Wait, wait a second...  I thought we couldn't put statements ahead of 
the super-call?

DIGRESSION...

Historically, this() or super() must be first in a constructor. This 
restriction was never popular, and perceived as arbitrary. There were a 
number of subtle reasons, including the verification of invokespecial, 
that contributed to this restriction.  Over the years, we've addressed 
these at the VM level, to the point where it becomes practical to 
consider lifting this restriction, not just for records, but for all 
constructors.

Currently a constructor follows a production like:

     [ explicit-ctor-invocation ] statement*

We can extend this to be:

     statement* [ explicit-ctor-invocation statement* ]

and treat `this` as DU in the statements in the first block.

...END DIGRESSION

OK, so we can put a statement ahead of the super-call.  But this 
explicit declaration is awfully verbose.  We can trim this by:
  - Allow the compiler to infer the signature for the default 
constructor, if none is provided;
  - Provide a shorthand for "just do the default initialization".

Now we get:

     public B {
         if (b <= 0)
             throw new IllegalArgumentException("b");
         default.this(a, b);
     }

There's still some repetition here; it would be nice if the default 
initialization were inferred as well.  Which leads to a question: if we 
have a record constructor with no explicit constructor call, do we do 
the default initialization at the beginning or the end?  In other words, 
does this:

     public B {
         if (b <= 0)
             throw new IllegalArgumentException("b");
     }

mean

     public B {
         if (b <= 0)
             throw new IllegalArgumentException("b");
         default.this(a, b);
     }

or this:

public B {
default.this(a, b);
if (b <= 0)
             throw new IllegalArgumentException("b");
     }

The two are subtly different, and the difference becomes apparent if we 
want to normalize arguments or make defensive copies, not just validate:

public B {
         if (b <= 0)
             b = 0;
     }

If we put our implicit construction at the beginning, this would be a 
dead assignment to the parameter, after the record was initialized, 
which is almost certainly not what the user meant.  If we put it at the 
end, this would pick up the update.  The former seems pretty 
error-prone, so the latter seems attractive.

However, this runs into another issue, which is: what if we have 
additional fields?  (We might disallow this, but we might not.)  Now 
what if we wanted to do:

     record B(int a, int b) {
         int cachedSum;

         B {
             cachedSum = a + b;
         }
     }

If we treat the explicit statements as occuring before the default 
initialization, now `this` is DU at the point of assigning `cachedSum`, 
and the compiler tells us that we can't do this.  Of course, there's a 
workaround:

B {
default.this(a, b);
cachedSum = a + b;
         }

which might be good enough. (Note that we'd like to be able to extend 
this ability to constructors of classes other than records eventually, 
so we should work out the construction protocol in generality even if 
we're not going to do it all now.)

Is `default.this(a, b)` still too verbose/general/error-prone?  Would 
some more invariant marker ("do the default thing now") be better, like:

     B {
         new;
this.cachedSum = a + b;
     }



So, summarizing:
  - We're OK with Foo { ... } as shorthand for the default constructor?
  - Where should the implicit construction go -- beginning or end?
  - Should there be a better idiom other than default.this(args) for "do 
the explicit construction now"?




More information about the amber-spec-observers mailing list