Looking at Java 22: Statements before super

 · 5 min
AI-generated by DALL-E

The first (preview) feature of Java 22 I want to talk about is one I’m quite excited about!

JEP 447 introduces a significant change by relaxing the strict rules for constructors. It finally allows (certain) statements to be executed before calling the super(...) call.


Calling super(…) first

Inheritance plays a fundamental role in Java, yet there’s a particular constraint on constructors that can be rather bothersome: every subclass constructor must invoke super(...) or delegate to another constructor using this(...).

There are instances where this rule might not seem immediately clear. If the superclass has an argument-less constructor, either implicitly if there are no other constructors or explicitly, it gets called by any sub-class constructor even if there’s no super() call.

java
public class SuperClass {

  public SuperClass() {
    System.out.println("SuperClass");
  }
}

public class SubClass extends SuperClass {

  public SubClass() {
    System.out.println("SubClass");
  }
}


new SubClass();

// OUTPUT:
// SuperClass
// SubClass

From a technical perspective, the implicit super(...) call, implicit or otherwise, is required to initialize the type above, ensuring it’s fully initialized. Even SuperClass calls super() to initialize the (implicitly) extended java.lang.Object.

Nevertheless, the call-/statement-order restriction is always there and makes certain use-/edge cases quite convoluted or even impossible, such as preparing or validating arguments before passing them to the superclass constructor.

Oftentimes, sub class constructors accept additional data or data in a different form and need to prepare or transform them. Given the rule that the super constructor must be called first, we either need to do additional work after calling super(...), while calling super(...), or not using a constructor at all:

  • Initializing first, then validating, which leads to the creation of unnecessary instances.
  • Delegating to static method, which can make constructor invocations cumbersome and is not suitable for every scenario.
  • Creating static factory method and making the original constructor private
java
public class PositiveBigInteger extends BigInteger {

  // VALIDATE AFTER CALLING SUPER

  public PositiveBigInteger(long value) {
    super(value);
    if (value <= 0L) {
      throw new IllegalArgumentException("non-positive value");
    }
  }


  // VALIDATE WITH AUXILIARY METHOD

  public PositiveBigInteger(long value) {
    super(verifyPositive(value));
  }

  private static long verifyPositive(long value) {
    if (value <= 0L) {
      throw new IllegalArgumentException("non-positive value");
    }
    return value;
  }


  // FACTORY METHOD (+ making constructor private)

  public static PositiveInteger(long value) {
    if (value <= 0L) {
      throw new IllegalArgumentException("non-positive value");
    }
    return new PositiveInteger(value);
  }
}

The same issues arise if we want to prepare incoming arguments further before passing them to the superclass.

We might even run into these issues without subclassing at all, as Records don’t allow any argument modification before calling this(...) in a non-canonical constructor.

No matter which route is taken, the resulting code tends to become more complex and less intuitive than desired.

JEP 447 is going to remedy a lot of these pain points!


JEP 447: Statements before super(…)

The reason for requiring to call super(...) first is found in the Java Language Specification (JLS §8.8.7). Constructors are defined as follows:

ConstructorBody:
  { [ExplicitConstructorInvocation] [BlockStatements] }

Furthermore, the argument list of a constructor call runs in a static context (JLS §8.1.3) and is, therefore, quite restrictive.

JEP 447 revises the Java Language Specification (JLS) for constructors:

ConstructorBody:
  { [BlockStatements] }
  { [BlockStatements] ExplicitConstructorInvocation [BlockStatements] }

The change introduces a prologue of statements before an explicit constructor call. Rather than amending or changing the concept of static context, a new pre-construction context with rules similar to instance methods was added to relax one of the biggest no-nos of constructors: accessing the instance under construction.

In essence, accessing any unqualified this expression, or using super qualified field access, method invocation, or method reference is still forbidden. The JEP itself describes what is available and what’s forbidden as tricky, so make sure to check out the “Description” section to get a better picture.

This is the previous PositiveBigInteger with JEP 447 enabled:

java
public class PositiveBigInteger extends BigInteger {

  public PositiveBigInteger(long value) {
    if (value <= 0L) {
      throw new IllegalArgumentException("non-positive value");
    }
    super(value);
  }
}

Preparing constructor arguments becomes more readable, too:

java
public class SuperClass {

  public SuperClass(String element) {
    System.out.println("SuperClass: " + element);
  }
}

public class SubClass extends SuperClass {

  public SubClass(List<String> data) {
    String element;
    if (data != null && !data.isEmpty()) {
      element = data.get(0).toLowerCase();
    } else {
      element = "<n/a>";
    }

    super(element);
  }
}

new SubClass(List.of("One", "Two", "Three"));
// => SuperClass: one

new SubClass(null);
// => SuperClass: <n/a>

Records and Enums also benefit from the new pre-construction context, even without supporting inheritance.

We can now use statements before calling an alternative/convenience constructor:

java
public record Email(String local, String domain) {

  public Email(String fqda) {
    Objects.requireNonNull(fqda);
    var parts = fqda.split("@");
    if (parts.length != 2) {
      throw new IllegalArgumentException("...");
    }

    this(parts[0], parts[1]);
  }
}

Conclusion

By introducing the pre-construction context, this JEP gives us an improved way of dealing with constructor invocation. Many more convoluted approaches to solving argument preparation and validation will become more straightforward and easier to grasp.

And the best thing, it doesn’t require any changes to the JVM itself, as the “call super(...) first restriction was a historical artifact, not a JVM limitation. That’s why relaxing the JLS was enough to give us this awesome feature!

If you want to try it out today, get yourself a copy of Java 22 and use the --enable-feature flag on one of the tools (jshell, java, javac). Depending on which one you choose, you might need additional arguments, like --source or --release, but the tool itself will tell you.


A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!
Available in English, Polish, and soon, Chinese.

Resources

Looking at Java 22