Draft Spec for Second Preview of Flexible Constructor Bodies (JEP 482)

Maurizio Cimadamore maurizio.cimadamore at oracle.com
Mon Jun 10 17:10:05 UTC 2024


On 10/06/2024 17:32, Archie Cobbs wrote:

> On Mon, Jun 10, 2024 at 4:24 AM Maurizio Cimadamore 
> <maurizio.cimadamore at oracle.com> wrote:
>
>     We basically have classes which have no enclosing instances, but
>     where enclosing members can be accessed (via other means).
>
>
> Hah, you've already got me in a screeching halt on the first sentence 
> here, because I'm inferring a different meaning of "enclosing 
> instance" than what I knew (and you also throw in "enclosing members", 
> phew :)
>
> Just so we're on the same page - with respect to the anonymous 
> Runnable class below, how would you describe "ClassA.this" and 
> "ClassB.this"?

The anon Runnable has two enclosing instances. One of type ClassB, one 
of type ClassA. The former cannot be accessed, the latter can.

>
> class ClassA {
>     class ClassB {
>         ClassB() {
>             this(new Runnable() {
>                 public void run() {
>                     ClassA.this.hashCode();
>                 }
>             });
>         }
>         ClassB(Runnable r) {
>         }
>     }
> }
>
>
>     Historically, the JLS used to treat this/super calls as a static
>     context (8.8.7.1).
>
>
> That's what the JLS said, but of course that's not what the compiler 
> ever implemented (JDK-8301649).

Sure. What about ecj? What did that compiler implement? (This is more a 
question for Stephan).

What I’m trying to say is that “because javac does it” is not always a 
good reason for doing something.

In this particular case, the contrived nature of the examples involved, 
and the complexity of the resulting translation scheme make me question 
as to what the right choice should be.

>     And then JLS also used to say that inner classes defined in a
>     static context had no enclosing instance (8.1.3).
>
>     So, for this particular JEP, I think there's a choice in front of us:
>
>     * do we keep JLS as is (and fix javac to do what the spec has
>     always said the behavior was) ?
>     * or, do we keep javac as is and then tweak the JLS to model what
>     the implemented behavior is?
>
>     The latter path seems to have been chosen. Do we have a feeling
>     that local classes in pre-construction context will be radically
>     more common than they are today? Or are there other reasons behind
>     this decision?
>
> Here is my own understanding of the reasoning behind choosing option 
> #2 (others may differ).
>
> First of all, the question of how to treat classes in early 
> construction contexts is not new - it's just become a lot more obvious 
> because of the new flexibility offered in the JEP.
> The "early construction context" already exists in constructor 
> parameters, although of course more shoe-horning required doing it the 
> old way.
>
> For example this code compiles fine in JDK 17:
>
> class EarlyLocal {
>     int x;
>     class Inner {
>         Inner(int x) {
>             this(switch (x) {
>                 default -> {
>                     class Local { { EarlyLocal.this.hashCode(); } }
>                     yield 0.0;
>                 }
>             });
>         }
>         Inner(double x) {
>         }
>     }
> }
>
> So that means there is some amount of code out there (unknown how 
> much) which is, for example, referencing 2nd outer instances from 
> anonymous classes that are created inside constructor parameters (a 
> more likely example of that is the one given earlier in this email 
> thread (May 30th, "class Outer")).

This means “there might be” some amont of code. Some initial analysis 
against Maven found in fact /no uses/. But I will need to double check 
the results, and also include lambda expressions in the mix.

My beef with this thread is that we keep using the fact that it “works 
in javac” as a proof that the feature has to be designed this way.

Here’s a recently discovered counter-example:

|class Outer { interface A { } interface B { } class Inner1 { Inner1() { 
this(new A() { class Inner2 { Inner2() { this(new B() { { m(); g(); } 
}); } Inner2(Object o) { } } void m() { } }); } Inner1(Object o) { } } 
void g() { } } |

This does NOT work with javac, in fact it never did (I went back as far 
back as Java 7, but I have no reason to believe it ever worked). This 
example defeats the very assumption javac relies upon when translating 
local classes - that there is a chain of enclosing instances, reachable 
using some hidden field. In this case there’s /two/ enclosing instances, 
and one is not reachable by the other.

We can fix this by making the implementation even more complex. Or we 
can step back, and ask ourselves: how many developers, when seeing the 
above code, will immediately grasp what that code means? My bet is that 
nobody /really/ wants to write code that looks like this. Surely nobody 
wants to read it (I don’t!).

>
> A second reason is that access to 2nd & further outer instances is 
> something developers understand and expect, and denying it to them for 
> no particularly good reason (my opinion) in one place (early 
> construction) but not anywhere else will strike them as arbitrarily 
> inconsistent and annoying. Putting this another way, developers 
> already have an intuitive understanding that you can't access 'this' 
> prior to superclass construction, so that exception is already baked 
> into their lives. If we make early construction contexts truly static 
> (option #1), then we're creating yet another exception they will have 
> to learn and work around. Contrast with option #2 which just extends 
> everything they already know and expect in a natural way.

The example above has nothing intuitive about it IMHO.

The problem is that we’re saying, /simultaneously/ that:

  * it is an error to use |this| in a pre-construction context
  * it is ok for a local class in a pre-construction context to use
    enclosing instances other than “problematic ones”

This is IMHO what leads to non-intuitiveness. I claim that the mental 
model developers have about encloising instances is that either there is 
an innermost enclosing instance, or there’s none. And if there is one, 
all outer enclosing instances are reachable, somehow, via the innermost 
instance. The model we’re proposing in #2 is different, and amounts at 
saying that a class can have N enclosing instances, all potentially 
unrelated from each other.

So, if A contains B, and B contains C, we can end up with cases where C 
can access the enclosing instance for A, whereas B cannot. This seems to 
violate a natural, intuitive, and compositional behavior (e.g. if both C 
and B have enclosing instances, then “surely” A must be the type an 
enclosing instance with respect to both).

>     Perhaps the tacit understanding was that "javac got this right",
>     but looking at the increasing numbers of bugs filed recently in
>     this area, and after looking more at how the code works, it seems
>     that javac doesn't have a very principled way to get there, and it
>     is in fact rather easy to come up with examples which defeats
>     javac's translation strategy.
>
> I agree that the increasing number of bugs is alarming. But I 
> attribute this to poor internal documentation and years of early 
> construction fixes. If you look back at Jira historically you'll see a 
> lot of bugs that relate specifically to early construction context 
> issues (in constructor parameters, obviously). The NOOUTERTHIS flag is 
> an example of how these got fixed with (arguably) a quick hack rather 
> than a more principled refactoring. IMHO, saying that these are 
> reasons to downgrade the spec is the tail wagging the dog.

I’m not saying we shouldn’t do it because it’s hard to implement.

I’m saying that the model put forward by the spec is (a) not very 
intuitive (despite what you seem to claim) and (b) not a very useful 
generalization.

My point here is that I dare to find a developer that in 2 seconds will 
be able to determine whether the code I wrote above should compile or not.

And if developers should spend 10 minutes chasing down the spec to 
understand what the program is supposed to mean, are we sure we want 
them to write code like this in the first place?

So, while we can spend resources in chasing down every single issue in 
this area, this feels, I admit, not a great use of our collective time 
(which I’m sure is limited).

Maurizio

>
>
> -Archie
>
> -- 
> Archie L. Cobbs

​
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-spec-experts/attachments/20240610/0b7074e6/attachment-0001.htm>


More information about the amber-spec-experts mailing list