JDK-8351274 : javac to null check immediately enclosing instance in inner class constructors
  • Type: CSR
  • Component: tools
  • Sub-Component: javac
  • Priority: P4
  • Status: Closed
  • Resolution: Approved
  • Fix Versions: 25
  • Submitted: 2025-03-05
  • Updated: 2025-03-25
  • Resolved: 2025-03-25
Related Reports
CSR :  
Description
Summary
-------

Currently, an inner class instance with `null` enclosing instances can be created by reflection, method handles, or direct bytecode invocation. javac's translation of inner class constructors will add a null check to the incoming immediately enclosing instance argument (unless this argument is delegated to another this() constructor call) to block such creations for target releases beginning in 25. This default behavior can be overridden with the undocumented `nullCheckOuterThis` flag like `-XDnullCheckOuterThis=(true|false)`. 

Problem
-------

JLS provides that various use sites of inner class constructors should include null checks if the incoming immediately enclosing instance may be `null`, including for qualified instance creation expressions (JLS 15.9.4) and superconstructor invocations (JLS 8.8.7.1 "if the superclass constructor invocation is qualified"). Then, JLS assumes the enclosing instances are non-null in the usages in inner classes.

However, JLS does not mandate any check in the declaration site of the inner class constructors. As a result, core reflection, method handles, or direct bytecode invocation may pass `null` for inner class constructors, and the usages of the enclosing instances later may see `null` and fail with an NPE.

Solution
--------

Beginning in target release 25, javac will start emitting null checks in inner class constructors that calls a superclass constructor. Such constructors store to the synthetic field that captures the immediately enclosing instance. Other constructors all delegate to constructors that call superclass constructors.

This is purely an implementation artifact with no JLS changes. In addition, these null checks will be emitted, even if the enclosing instance is unused and not stored into a field.

These null checks will be emitted for target releases beginning in 25, and not emitted for earlier releases. Users can use the undocumented `nullCheckOuterThis` flag to override this default behavior, like `-XDnullCheckOuterThis=(true|false)`.

Note that the original javac-generated null checks for the JLS 15.9.4 and 8.8.7.1 provisions are kept as-is. They currently causes an NPE before other exceptions can arise, such as any run-time `LinkageError` or any error thrown by the inner class constructors (such as one that calls a super constructor with a static method that throws an unchecked exception), which the checks added by this solution do not cover.

Specification
-------------

No change.

It seems the word "instance" in the JLS means the presence of an object, as in "class instance", as opposed to null references. Therefore, null-checking a parameter that must pass an instance makes sense.
Comments
Moving to Approved, subject to a release note being written. I also recommend this change be raised in the quality-discuss newsletter.
25-03-2025

I have made this null check enabled by default for targets beginning in 25. This default setting can be overridden by an undocumented javac option `-XDnullCheckOuterThis=(true|false)` for any target release.
19-03-2025

Hmm. Moving to Provisional, not Approved. [~liach], I think there needs to be a more extensive compatibility assessment here. At a minimum, the new null-checks should *not* be enabled under --source/--target settings other than the latest to avoid future breakage of users who recompile to run under older releases. Additionally, it may be helpful to have a javac hidden option to enable/disabled these checks.
18-03-2025

I'm comfortable with the idea that: - The language does not specify what the generated code does with an enclosing instance parameter—just that the parameter exists - The generated code can reasonably assume that an appropriate input will be non-null - Our implementation of generated code would be well-served by consistently throwing an exception, rather than having any exceptions depend on whether the parameter needs to be stored in a field, downstream uses of the field, etc.
06-03-2025

I've noticed that it's possible to construct a serialization stream that deserializes with a null enclosing instance, using a record shaped like this: ``` record Outer(Outer.Inner[] inner) implements Serializable { class Inner implements Serializable { public String toString() { return "Inner(%s)".formatted(Outer.this); } } } var broken = new Outer(new Outer.Inner[1]); broken.inner()[0] = broken.new Inner(); ``` Old behavior: serialize & deserialize, the array holds an Inner with a `null` enclosing instance, and no way to "fix" that instance New behavior: serialize & deserialize, deserialization throws an exception I think the code is an abuse of records (which are not designed for such cycles), and should someone stumble on this, they're better served by an exception (new behavior) than a silently-broken object (old behavior).
06-03-2025