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