flexible constructor bodies
statements before super(), finally.
- chapter
- chapter 08.09
- reading time
- 7 min read
- java 25+
since java 25 (JEP 513), a constructor can run statements before super(...) or this(...). that part is the prologue: it can validate arguments, compute values for the superclass constructor, and assign its own fields, all before any superclass code runs.
what it still can't do is touch the object: no reading fields, no calling instance methods, no passing this anywhere. the object isn't built yet, and the compiler makes sure nobody finds out.
the picture
you know the rule "super(...) must be the first statement in a constructor"? it was there from java 1.0, and it made simple things awkward. want to check an argument before handing it to the superclass? you couldn't, so you wrote a static helper and called it inside the super(...) arguments, or you let the superclass do work with a bad value and threw afterwards.
think of building a house. the old rule said the foundation crew (the superclass) has to start before you've even looked at the plans: you can't check the plot, can't measure anything, just pour concrete and hope. the new rule lets you survey the land and check the permits first, as long as you don't try to move furniture into a house that doesn't exist yet.
class Employee extends Person {
Employee(String rawName) {
super(clean(rawName));
}
private static String clean(String raw) {
if (raw == null || raw.isBlank())
throw new IllegalArgumentException("no name");
return raw.strip().toUpperCase();
}
}class Employee extends Person {
Employee(String rawName) {
if (rawName == null || rawName.isBlank())
throw new IllegalArgumentException("no name");
String name = rawName.strip().toUpperCase();
super(name);
}
}same behaviour, but the right side reads top to bottom: check, prepare, construct. compile the right side with --release 24 and javac 25 refuses:
error: flexible constructors is not supported in -source 24prologue and epilogue
with statements allowed before super(...), a constructor has two halves. the prologue runs before the superclass is constructed, the epilogue after. across a hierarchy that gives a neat order: prologues run from the subclass up, epilogues from the top down:
hover a stage.
C extends B extends A, each printing in its prologue and its epilogue. this is the real output of new C() on jdk 25.the real win: fields set before the superclass looks
here's the bug that the old rule made unavoidable. Person's constructor calls show(), an overridable method, and Employee overrides it to print a field of its own. switch between the two orders:
- 1class Person {
- 2 final String name;
- 3 final int age;
- 4
- 5 Person(String name, int age) {
- 6 this.name = name;
- 7 this.age = age;
- 8 show();
- 9 }
- 10
- 11 void show() { IO.println(name + ", " + age); }
- 12}
- 13
- 14class Employee extends Person {
- 15 final String officeId;
- 16
- 17 Employee(String name, int age, String officeId) {
- 18 if (age < 18) throw new IllegalArgumentException();
- 19 this.officeId = officeId;
- 20 super(name, age);
- 21 }
- 22
- 23 @Override
- 24 void show() { IO.println(name + ", office " + officeId); }
- 25}
- 26
- 27void main() {
- 28 new Employee("ada", 36, "CAM-FORA");
- 29}
·main is about to build an Employee.
- 1class Person {
- 2 final String name;
- 3 final int age;
- 4
- 5 Person(String name, int age) {
- 6 this.name = name;
- 7 this.age = age;
- 8 show();
- 9 }
- 10
- 11 void show() { IO.println(name + ", " + age); }
- 12}
- 13
- 14class Employee extends Person {
- 15 final String officeId;
- 16
- 17 Employee(String name, int age, String officeId) {
- 18 if (age < 18) throw new IllegalArgumentException();
- 19 super(name, age);
- 20 this.officeId = officeId;
- 21 }
- 22
- 23 @Override
- 24 void show() { IO.println(name + ", office " + officeId); }
- 25}
- 26
- 27void main() {
- 28 new Employee("ada", 36, "CAM-FORA");
- 29}
·same program, with the field assigned after super(...), which was the only legal place before java 25.
class Person {
final String name;
final int age;
Person(String name, int age) {
this.name = name;
this.age = age;
show(); // an overridable call: careful
}
void show() { IO.println(name + ", " + age); }
}
class Employee extends Person {
final String officeId;
Employee(String name, int age, String officeId) {
if (age < 18) throw new IllegalArgumentException("too young: " + age);
this.officeId = officeId; // before super(...): new in java 25
super(name, age);
}
@Override
void show() { IO.println(name + ", office " + officeId); }
}
void main() {
new Employee("ada", 36, "CAM-FORA");
new Employee("bob", 16, "CAM-FORA");
}ada, office CAM-FORA Exception in thread "main" java.lang.IllegalArgumentException: too young: 16
the second new throws before Person ever runs. fail fast, no half-built object, no wasted superclass work. (calling overridable methods from a constructor is still a bad idea, effective java item 19 says so for good reasons. but now the subclass can at least defend itself.)
what the prologue still can't do
the object is allocated (memory, zeroed fields) but not constructed, so anything that could observe it is off limits. every one of these is rejected by javac 25:
| in the prologue | javac 25 says |
|---|---|
read a field: int y = a; | cannot reference a before supertype constructor has been called |
call an instance method: hashCode(); | cannot reference hashCode() before supertype constructor has been called |
use this: System.out.println(this); | cannot reference this before supertype constructor has been called |
call a superclass method: super.m(); | cannot reference super before supertype constructor has been called |
| assign a field that has an initializer | cannot assign initialized field 's' before supertype constructor has been called |
return; | 'return' not allowed before explicit constructor invocation |
if (b) super(1); else super(2); | explicit constructor invocation not allowed here |
what is allowed: locals, arguments, static methods and fields, throwing, and assigning your own fields that don't have an initializer. a lambda in the prologue counts as touching this if it mentions a field, so that's out too.
record Range(int lo, int hi) {
Range(String text) { // "3-7"
String[] parts = text.strip().split("-");
this(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
}
}
IO.println(new Range(" 3-7 "));why the old rule existed, and why it could go+
the JVM never required super(...) to come first. its rules are looser: on every code path the superclass constructor is called exactly once (and not inside a try), and until then the half-built object is off limits except for assigning fields. javac itself has always relied on that, for instance to store the hidden outer-instance field of an inner class before calling super. the "first statement" rule was the language being stricter than the JVM, because "first" was easy to check and guaranteed "not used". JEP 513 swaps it for the real requirement, checked in what the spec now calls an early construction context, and needed no JVM change at all. it previewed three times: as "statements before super(...)" (JEP 447, java 22), as "flexible constructor bodies" with early field assignment (JEP 482, 23, and JEP 492, 24), and went final unchanged in java 25.
- java 25: statements may come before
super(...)orthis(...). - the prologue validates, prepares arguments, and may assign its own uninitialized fields.
- it can't touch the object: no field reads, no instance methods, no
this, nosuper.x. - prologues run subclass-first, epilogues superclass-first.
- set fields in the prologue and an overridden method called by the superclass sees real values.