Loosening requirements for super() invocation

Brian Goetz brian.goetz at oracle.com
Mon Jan 23 20:01:23 UTC 2023


> OK, JEP filed here: https://bugs.openjdk.org/browse/JDK-8300786
> Fire away :) 

Incoming fire....

Overall this is a pretty good job on a first pass!  Some comments, 
mostly on presentation, but some on substance.

> Summary
> -------
>
> No longer require `super()` and `this()` to appear first in a constructor.

Framing it this way is negative, and requires users to understand the 
restriction that you are loosening.  Instead, a more positive framing is:

 > Allow statements that do not reference the instance being created to 
appear before the `this` or `super` call in a constructor.

> Goals
> -----
>
> Change the Java Language Specification and make corresponding changes 
> to the Java compiler so that:

Changing the JLS and implementation are consequences of changing the 
_language_, so start there.  Also, "don't change the meaning of existing 
programs" is table stakes, so we don't call that out as a goal.

> * `super()` and `this()` no longer must appear as the first statement 
> in a constructor
> * The language preserves existing safety and initialization guarantees 
> afforded to constructors
> * Existing programs continue to compile and function as they did before

* Allow statements that do not reference the instance being created to 
appear before the `this` or `super` constructors calls in a constructor.
* Preserve existing safety and initialization guarantees for constructors

> Non-Goals
> ---------
>
> __Modifications to the JVM.__ These changes may prompt reconsideration 
> of the JVM's current restrictions on constructors, however, in order 
> to avoid unnecessary linkage between JLS and JVM changes, any such 
> modifications should be proposed in a follow-on JEP. This JEP assumes 
> no change to the current JVM behavior.

I would add a non-goal that we are not necessarily going to make the JLS 
align with the JVMS here either; the two address different domains and 
may reasonably differ.  For example, the JVMS allows multiple writes to 
the same final field, but the language does not. We are not going to 
change that here, and that's OK.

> Motivation
> ----------
>
> Currently, the Java language requires that invocations of `this()` or 
> `super()` appear as the first statement in a constructor.

You should follow up with the motivation for this requirement, and then 
can talk about how it doesn't achieve all those goals and that's why 
we're you're proposing improving it.  The goal was to ensure that the 
superclass representation is at least in a known state before we start 
operating on it, but as you'll show, this isn't really enough to prevent 
us from accessing incompletely initialized instances.  So it was a good 
try, with good motivation, but we still have a problem with 
initialization safety, *and* certain idioms are hard to express.

I would then lead into the idioms that are hard to express, and the 
workarounds users are driven to as a result, to show how the cure may be 
worse than the disease.

> However, the Java Virtual Machine actually allows more flexibility:

I would defer this section to later; most Java programmers are only 
vaguely aware of the JVM spec or how it aligns or fails to align with 
the JLS (or why).  Instead, after providing an example of the hoops we 
force users to jump through, we can observe that there is a safe 
alternative (allow computation that doesn't touch `this` except in the 
trivial case of field writes) and that, yay, the JVM already accepts 
such bytecode without modifying the JVMS.

> There is also a practical motivation, which is that it's often 
> convenient to be able to do "housekeeping" before invoking `super()` 
> or `this()`.

This is the argument I would lead with: it makes users jump through 
hoops for no incremental safety.

> Here's a somewhat contrived example:

Perhaps you should lead with a less contrived example, lest readers 
think that this whole JEP is about solving contrived problems. (This is 
not an idle concern!  We get this all the time.)  Or maybe it's not a 
contrived example -- it seems to me that your BPV constructor is 
following the "fail fast" imperative outlined in Effective Java.  So 
maybe not so contrived.

But it is useful, in any case, to chracterize _what kind of code_ we 
might want before this/super.  Which I think comes down to:
  - validating inputs
  - computing complex, possibly-failing inputs into the super 
constructor, or
  - computing values that will be used _multiple times_ in the super 
constructor invocation

> Another reason is to provide a way to avoid bugs caused by a 'this' 
> escape in a superclass constructor. A 'this' escape is when a 
> superclass constructor does something that could cause a subclass 
> method to be invoked before the superclass constructor returns; in 
> such cases the subclass method would operate on an incompletely 
> initialized instance.

Yes, this is a nice side effect of "do the work that might fail before 
you expose `this`."  A good thing to end the motivation on.

> Description
> -----------
>
> ## Language Changes
>
> The JLS will be modified as follows:
>
> * Remove the requirement that `super()` or `this()` appear as the 
> first statement in a constructor
> * Add the requirement that, in any constructor with explicit `super()` 
> and/or `this()` invocations, either `super()` or `this()` must be 
> invoked exactly once (assuming the constructor returns normally). This 
> may be specified economically by stating that the compiler treats 
> superclass initialization like a non-static blank final field.

A more precise way to state this is that:

  - The `this` reference is considered DU on entry to the constructor;
  - The `this` reference is considered DA after a this/super call;
  - At a this/super call, `this` must be DU;
  - If no this/super call is present in the constructor body, it is 
treated as if that the first line of the constructor is `super()`;

Plus some wording about the use of `this` implicitly when it is DU.

> * Add the requirement that `super()` and `this()` may not appear 
> within any `try { }` block

if `this` is DA/U on entry to a try block, it must be DA/U on exit from 
that try block

> * Specify that non-static field initializers and initialization blocks 
> are executed immediately after `super()` invocation, wherever it occurs
>
> Note: there is no change to the implicit addition of `super()` at the 
> beginning of any constructor having no explicit `super()` or `this()` 
> invocation.
>
> ### `try { }` Blocks
>
> The restriction that `super()` and `this()` may not appear inside a 
> `try { }` block comes from the JVM itself, and is due to how StackMaps 
> are represented. The logic is that when a superclass constructor 
> throws an exception, the new instance on the stack is neither fully 
> uninitialized nor fully initialized, so it should be considered 
> unusable, and therefore such a constructor must never return. However, 
> the JVM doesn't allow the bytecode to discard the unusable instance 
> and throw another exception; instead, it doesn't allow it to exist on 
> the stack at all. The net effect is that constructors can't catch 
> exceptions thrown by superclass initialization, even if rethrown.
>
> ### Initialization Order
>
> The JLS specifies that field initializers and initialization blocks 
> execute after superclass initialization via `super()`. So this class:
>
>     class Test1 {
>         final int x;
>         {
>             x = 123;
>         }
>         public Test1() {
>             super();
>             this.x = 456;
>         }
>     }
>
> generates this error:
>
>     Test1.java:8: error: variable x might already have been assigned
>             this.x = 456;
>                 ^
>
> However, now that `super()` can appear anywhere in a constructor, an 
> assignment in an initializer block can now happen after an earlier 
> assignment in a constructor. So this class:
>
>     class Test1 {
>         final int x;
>         {
>             x = 123;
>         }
>         public Test1() {
>             this.x = 456;
>             super();
>         }
>     }
>
> will now generate this error:
>
>     Test1.java:4: error: variable x might already have been assigned
>             x = 123;
>             ^
>
> As before, initializers and initialization blocks happen immediately 
> after superclass initialization, which happens when `super()` is 
> invoked. But now this can be anywhere in the constructor.
>
> One might ask why not move initializers and initialization blocks to 
> the start of every constructor, but that doesn't work. First, they 
> could have early references (e.g., by invoking an instance method), 
> and second, the constructor might invoke `this()` and `super()` on 
> different code branches, so you'd be executing the initialization 
> twice in the `this()` case.
>
> ### Records
>
> Record constructors are subject to more restrictions that normal 
> constructors. In particular:
>
> * Canonical record constructors may not contain any explicit `super()` 
> or `this()` invocation
> * Non-canonical record constructors may invoke `this()`, but not `super()`
>
> These restrictions remain in place, but otherwise record constructors 
> benefit from these changes. The net change is that non-canonical 
> record constructors can now invoke `this()` multiple times, as long as 
> it is invoked exactly once along any code path.

I'd like to come back and think about the record constraints some more 
after we have nailed down the non-record story.

> ## Compiler Changes

Generally the JEP does not have to outline the compiler changes, as long 
as the language changes are clearly described.  However, if you see 
specific risks and concerns, they can be called out in the Risks 
section.  Most of the rest can move to a "implementation plan" document 
whose primary consumers will be the current maintainers of the compiler, 
to help build a shared underestanding of how to proceed.







More information about the amber-dev mailing list