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

Archie Cobbs archie.cobbs at gmail.com
Mon Jun 10 16:32:32 UTC 2024


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"?

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).


> 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")).

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.

> 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 agree this is confusing and we need better terminology. But I'm having
> trouble with the model that A is an enclosing instance of B, but A's
> members are not accessible in B. I understand why the spec does this - but
> it would be vastly simpler (at least from an implementation perspective)
> then to say that A is not in fact an enclosing instance of B, but that B
> still has some other enclosing instance (some enclosing class of A). A
> model such as this would inform the translation strategy quite clearly. In
> fact, I'm starting to think if we really want to go down the path of
> non-static local classes in pre-construction contexts, then javac should do
> things this way regardless of how things are specified, as that will make
> the translation strategy more uniform and less buggy than the one we have
> today.
>

This makes plenty of sense to me and I'd be happy with it.

Frankly I don't care what the terminology is, as long as it is
precise/unambiguous and universally agreed-to. We all need to be speaking
the same language (and that includes internal compiler method names,
variable names, and comments - with the caveat that this part is an
openjdk/javac discussion probably more appropriate for compiler-dev).

-Archie

-- 
Archie L. Cobbs
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-spec-experts/attachments/20240610/7af63f7a/attachment-0001.htm>


More information about the amber-spec-experts mailing list