Most Java developers have wanted to check a constructor argument before calling super(). Before Java 25, the compiler always said no, because the call to super(…) or this(…) had to be the first statement. Java 25 removes that rule. A constructor can now validate arguments, prepare values and set its own fields before it calls another constructor. The feature is called flexible constructor bodies, and it became final in Java 25 with JEP 513 after previews in Java 22, 23 and 24.
In short: on Java 25, we can put checks and simple computations before super(), as long as that code doesn’t touch the object being constructed.
In this tutorial, we’ll see why the old rule existed and what it cost us, then fix those costs with the new constructor body. We’ll finish with what the code before super() still can’t do, and why.
1. Why Java Required super() as the First Statement
Let’s start with the code we’ve always wanted to write. A savings account should reject a negative interest rate, so we check the rate before we call the superclass constructor:
public SavingsAccount(String id, double interestRate) {
if (interestRate <= 0) {
throw new IllegalArgumentException("Interest rate must be positive: " + interestRate);
}
this.interestRate = interestRate;
super(id);
}
The Java 21 compiler rejects it:
SavingsAccount.java:16: error: call to super must be first statement in constructor
super(id);
^
The rule comes from how Java builds an object. A SavingsAccount object contains the fields of all its classes: those of Object, those of its superclass Account, and its own. Java fills them in from the top down. The Account constructor sets up the Account part first, and only then does the SavingsAccount constructor continue. Requiring super() first was the simplest way to guarantee that order: no subclass code could run on an object whose superclass part didn’t exist yet.
The rule was safe, but too blunt. Our check doesn’t touch the object at all; it only looks at an argument. Still, the compiler blocked it, and that had real costs.
2. What the Old Rule Cost Us
To see those costs, we’ll use a small Account superclass whose constructor does two visible things. It counts the opened accounts in OPENED, so we can tell whether its constructor ran. It also builds a summary by calling describe(), a method that subclasses can override:
public class Account {
static final AtomicInteger OPENED = new AtomicInteger();
private final String id;
private final String summary;
public Account(String id) {
this.id = id;
OPENED.incrementAndGet();
this.summary = describe();
}
public String describe() {
return "Account " + id;
}
// id() and summary() accessors
}
Now let’s write the savings account the way Java 21 forces us to, with super() first. We’ll call it LegacySavingsAccount. It adds the interest rate and overrides describe() to include it:
public class LegacySavingsAccount extends Account {
private final double interestRate;
public LegacySavingsAccount(String id, double interestRate) {
super(id);
if (interestRate <= 0) {
throw new IllegalArgumentException("Interest rate must be positive: " + interestRate);
}
this.interestRate = interestRate;
}
@Override
public String describe() {
return "Savings " + id() + " at " + interestRate + "%";
}
}
This looks harmless, but it has two problems. Each test below resets the counter with Account.OPENED.set(0) in a @BeforeEach method first.
2.1. The Check Runs After the Superclass Has Done Its Work
Let’s pass a negative rate. The check does reject it, but look at the counter:
assertThatThrownBy(() -> new LegacySavingsAccount("ACC-1001", -1.0))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Interest rate must be positive: -1.0");
assertThat(Account.OPENED.get()).isEqualTo(1);
The Account constructor already ran and counted an account that will never exist. Here it’s only a counter. In real code, the superclass might open a connection, register a listener or write to a database, all for an object that we’re about to throw away.
The usual workaround was to sneak the check into the argument list of super() with a static helper:
public HelperSavingsAccount(String id, double interestRate) {
super(checkRate(id, interestRate));
this.interestRate = interestRate;
}
private static String checkRate(String id, double interestRate) {
if (interestRate <= 0) {
throw new IllegalArgumentException("Interest rate must be positive: " + interestRate);
}
return id;
}
This fails before Account runs, but read it again: checkRate() receives the ID and returns it unchanged, only so that it can sit inside the super() call. The check is real, but the code hides it.
2.2. The Superclass Sees the Field Before It’s Set
The second problem is harder to spot. The Account constructor calls describe(), and because LegacySavingsAccount overrides it, Java runs the subclass version. But at that moment, the subclass constructor hasn’t reached the line that sets interestRate. Let’s see what the summary contains:
LegacySavingsAccount account = new LegacySavingsAccount("ACC-1001", 3.5);
assertThat(account.summary()).isEqualTo("Savings ACC-1001 at 0.0%");
assertThat(account.describe()).isEqualTo("Savings ACC-1001 at 3.5%");
The superclass constructor read interestRate while it still held its default value, 0.0. Calling describe() later gives 3.5, so the same final field showed two different values. No workaround in Java 21 can set a subclass field before super(), so this bug simply couldn’t be fixed inside the constructor.
Both problems have the same root: the subclass had no chance to run any code before the superclass constructor. That’s exactly what Java 25 changes.
3. Prologue and Epilogue in Java 25
Java 25 splits a constructor body into two parts, around the super(…) or this(…) call. The Java Language Specification and Oracle’s Flexible Constructor Bodies guide name them:
- Prologue: the statements before the explicit super(…) or this(…) call.
- Epilogue: the statements after that call, or the whole body when there is no explicit call.
Before we use the prologue, let’s make sure we understand when it runs. We’ll trace a three-level hierarchy of account types. Each constructor adds one line to a shared log before its super() call and one line after it:
public static class TracedAccount {
public TracedAccount(List<String> log) {
log.add("Account prologue");
super();
log.add("Account epilogue");
}
}
public static class TracedSavingsAccount extends TracedAccount {
public TracedSavingsAccount(List<String> log) {
log.add("SavingsAccount prologue");
super(log);
log.add("SavingsAccount epilogue");
}
}
public static class TracedFixedDepositAccount extends TracedSavingsAccount {
public TracedFixedDepositAccount(List<String> log) {
log.add("FixedDepositAccount prologue");
super(log);
log.add("FixedDepositAccount epilogue");
}
}
In TracedAccount, super() calls the Object constructor. Now let’s create the deepest class and look at the log:
List<String> log = new ArrayList<>();
new ConstructionOrder.TracedFixedDepositAccount(log);
assertThat(log).containsExactly(
"FixedDepositAccount prologue", "SavingsAccount prologue", "Account prologue",
"Account epilogue", "SavingsAccount epilogue", "FixedDepositAccount epilogue");
The prologues run from the subclass up to the top, and then the epilogues run back down. The superclass constructors still build the object from the top down, exactly as before. The only new thing is that each subclass gets a chance to run code first, which is precisely what our two problems needed.
4. Fixing Both Problems With a Prologue
Now we can go back to the constructor from section 1, the one Java 21 rejected. On Java 25, it compiles:
public SavingsAccount(String id, double interestRate) {
if (interestRate <= 0) {
throw new IllegalArgumentException("Interest rate must be positive: " + interestRate);
}
this.interestRate = interestRate;
super(id);
}
// interestRate() accessor and the same describe() override as LegacySavingsAccount
The check and the field assignment form the prologue. Let’s run the two tests again. First, the negative rate:
assertThatThrownBy(() -> new SavingsAccount("ACC-1001", -1.0))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Interest rate must be positive: -1.0");
assertThat(Account.OPENED.get()).isZero();
This time the counter stays at zero. The prologue threw before Account ran, and we didn’t need the static helper. Second, the summary:
SavingsAccount account = new SavingsAccount("ACC-1001", 3.5);
assertThat(account.summary()).isEqualTo("Savings ACC-1001 at 3.5%");
The prologue set interestRate before super(id), so when Account called describe(), the field already held 3.5. Calling overridable methods from a constructor is still a design we should avoid when we can, but when we inherit such a superclass, the prologue makes it safe.
To compile this code, the project needs maven.compiler.release (or javac –release) set to 25 or later.
5. Preparing Values Before Calling this()
The prologue helps with this(…) as well, when one constructor delegates to another. Let’s say accounts also arrive as text records, such as “ACC-2002; 4.25”. We want a constructor that parses the record and then calls the main constructor:
public SavingsAccount(String record) {
String[] parts = record.split(";");
String id = parts[0].trim();
double interestRate = Double.parseDouble(parts[1].trim());
this(id, interestRate);
}
Before Java 25, the parsing had to happen inside the argument list, for example this(parseId(record), parseRate(record)), with two static helpers that each split the same text. Now we split it once, give the pieces names, and delegate:
SavingsAccount account = new SavingsAccount("ACC-2002; 4.25");
assertThat(account.id()).isEqualTo("ACC-2002");
assertThat(account.interestRate()).isEqualTo(4.25);
Because we delegate to the main constructor, its validation still applies to the parsed rate. A record with a negative rate is rejected in exactly the same way.
6. What the Prologue Still Can’t Do
The prologue has one firm limit, and it follows from section 1. When the prologue runs, the superclass part of the object doesn’t exist yet. So the compiler blocks any prologue code that would use the object. The prologue may use the constructor’s arguments, local variables and static members, and it may assign its own class’s fields, but it can’t use the object under construction.
Let’s see what that means in SavingsAccount. Assume it also had a field with an initializer, private String currency = "USD";. Each of these statements, placed before super(id), fails to compile on Java 25:
| Statement in the prologue | Compiler error |
|---|---|
String text = describe(); | cannot reference describe() before supertype constructor has been called |
double copy = interestRate; | cannot reference interestRate before supertype constructor has been called |
System.out.println(this); | cannot reference this before supertype constructor has been called |
String copy = super.id(); | cannot reference super before supertype constructor has been called |
currency = "EUR"; | cannot assign initialized field ‘currency’ before supertype constructor has been called |
if (id.isBlank()) return; | ‘return’ not allowed before explicit constructor invocation |
Each line breaks the rule for a reason we can now explain:
- The first four use the object: calling its method, reading its field, passing this somewhere, or calling a superclass method. All of them would see a half-built object.
- currency already gets its value from its initializer, and Java runs field initializers only after super(). Assigning it earlier would be overwritten, so the compiler forbids it.
- A return would leave the constructor without ever building the superclass part. “Explicit constructor invocation” in that message simply means our super(…) or this(…) call.
Note the difference between assigning and reading. In section 4, we assigned interestRate in the prologue, which is allowed. Reading it there, even right after that assignment, is still an error.
7. Are Records and Enums Also Affected?
Yes, partly. A record‘s extra constructors must delegate to the canonical constructor with this(…), so they get the same benefit as SavingsAccount in section 5. For example, a Money record can parse and check a text value first:
public record Money(BigDecimal amount, String currency) {
public Money(String text) {
String[] parts = text.trim().split("\\s+");
if (parts.length != 2) {
throw new IllegalArgumentException("Expected '<amount> <currency>': " + text);
}
this(new BigDecimal(parts[0]), parts[1]);
}
}
A valid string creates the record, and an invalid one fails before the canonical constructor runs:
Money price = new Money("19.99 USD");
assertThat(price.amount()).isEqualByComparingTo(new BigDecimal("19.99"));
assertThat(price.currency()).isEqualTo("USD");
assertThatThrownBy(() -> new Money("19.99"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Expected '<amount> <currency>': 19.99");
The canonical constructor itself can’t call this(…) or super(…), so nothing changes there. Enum constructors work like the record’s extra constructors: they can have a prologue before an optional this(…) call, and they can never call super(…).
8. Conclusion
In this article, we saw that the old super()-first rule protected half-built objects, but also blocked harmless checks and let superclass constructors read unset subclass fields. Java 25 splits the constructor into a prologue and an epilogue, so we can validate, prepare values and assign our own fields before super() or this(). The prologue still can’t touch the object itself, so we should keep it to checks and plain computations. The other Java 25 changes are in Java 25 Features, and the two keywords in this vs super in Java.