Flexible Constructor Bodies (JEP 513): Validate Before super()
The Old Rule That Caused So Much Pain
From Java 1.0 through Java 24, a constructor that extended another class had one rigid rule:
super(...)orthis(...)must be the first statement in the constructor body.
This was enforced by the compiler. Not because of a deep technical reason — but because the JVM specification had always required that the superclass be fully initialized before the subclass could do anything with the object.
The result? Awkward, unreadable workarounds everywhere.
The Problem: A Concrete Example
Suppose you have a Shape base class and a Rectangle subclass. Rectangle needs to validate that width and height are positive before creating the object.
Before Java 25 — The Ugly Workaround
// Base class
public class Shape {
protected final double area;
public Shape(double area) {
this.area = area;
}
}
// Subclass — Java 8 through 24
public class Rectangle extends Shape {
private final double width;
private final double height;
public Rectangle(double width, double height) {
// super() MUST be first — we can't validate before calling it
super(width * height); // ← what if width or height is negative?
this.width = width;
this.height = height;
}
}
If you pass -5 as width, a Rectangle with area -5 * height gets created. You can add validation after super(), but by then the broken object already exists in memory.
The only workarounds were:
Workaround 1: Static factory method
public static Rectangle of(double width, double height) {
if (width <= 0 || height <= 0) throw new IllegalArgumentException("...");
return new Rectangle(width, height); // still calls the unprotected constructor
}
Nothing stops callers from using new Rectangle(-5, 10) directly.
Workaround 2: Helper static method in super() call
public Rectangle(double width, double height) {
super(validate(width, height)); // valid but deeply ugly
this.width = width;
this.height = height;
}
private static double validate(double width, double height) {
if (width <= 0 || height <= 0) throw new IllegalArgumentException("Dimensions must be positive");
return width * height;
}
This works but forces you to pack your validation into a static method just to squeeze it into the super() argument expression.
The Java 25 Solution
JEP 513 introduces the concept of a prologue: code that runs before the explicit constructor invocation (super() or this()).
After Java 25 — Clean and Direct
public class Rectangle extends Shape {
private final double width;
private final double height;
public Rectangle(double width, double height) {
// PROLOGUE — runs before super(), no "this" access allowed here
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException(
"Width and height must be positive, got: " + width + ", " + height
);
}
// Explicit constructor invocation
super(width * height);
// EPILOGUE — normal constructor body, "this" is available
this.width = width;
this.height = height;
}
}
The validation throws before the object is created. No half-constructed Rectangle ever exists.
Rules of the Prologue
The prologue can do a lot — but not everything. The constraint is simple: you cannot reference this (the object being created) because it doesn’t exist yet.
What you CAN do in the prologue
public class Circle extends Shape {
private final double radius;
public Circle(String radiusStr) {
// ✅ Call static methods
double r = parseRadius(radiusStr);
// ✅ Perform arithmetic
double area = Math.PI * r * r;
// ✅ Declare local variables
String unit = "cm²";
// ✅ Validate and throw
if (r <= 0) throw new IllegalArgumentException("Radius must be positive: " + r);
// ✅ Call super() with computed values
super(area);
this.radius = r;
}
private static double parseRadius(String s) {
try {
return Double.parseDouble(s);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Not a number: " + s, e);
}
}
}
What you CANNOT do in the prologue
public class BadExample extends Shape {
private double radius;
public BadExample(double r) {
// ❌ Cannot read instance fields — "this" doesn't exist yet
// System.out.println(this.radius);
// ❌ Cannot call instance methods — they'd operate on an uninitialised object
// double area = computeArea(r); // if computeArea() is non-static
// ❌ Cannot create instances of non-static inner classes
// Inner i = new Inner();
// ✅ CAN initialize fields (special case)
this.radius = r; // ← writing to a field IS allowed (see below)
super(Math.PI * r * r);
}
}
Wait — you can write to this.radius before super()? Yes! Field writes are allowed in the prologue as a special case. Field reads are not.
Field Initialization Before super()
This is the killer feature for final field validation patterns:
public class BankAccount extends AuditedEntity {
private final String accountId;
private final String owner;
public BankAccount(String accountId, String owner) {
// Validate and initialize fields BEFORE super()
if (accountId == null || accountId.isBlank()) {
throw new IllegalArgumentException("Account ID cannot be blank");
}
if (owner == null || owner.isBlank()) {
throw new IllegalArgumentException("Owner cannot be blank");
}
// Fields are set BEFORE super() runs
this.accountId = accountId.trim().toUpperCase();
this.owner = owner.trim();
// super() can now use the normalized values
super(this.accountId); // AuditedEntity uses accountId as its primary key
}
}
Before Java 25, this.accountId couldn’t be set before super(), so you had to pass raw accountId to super() and separately normalize it afterward — leaving a window where super()’s logic would see the unnormalized value.
Real-World Example: Chained Constructors
The prologue also works with this(...) (delegating constructors):
public class HttpClient {
private final String baseUrl;
private final int timeoutMs;
private final int maxRetries;
public HttpClient(String baseUrl, int timeoutMs, int maxRetries) {
if (baseUrl == null || !baseUrl.startsWith("http")) {
throw new IllegalArgumentException("Invalid base URL: " + baseUrl);
}
if (timeoutMs <= 0) throw new IllegalArgumentException("Timeout must be positive");
if (maxRetries < 0) throw new IllegalArgumentException("Retries cannot be negative");
this.baseUrl = baseUrl;
this.timeoutMs = timeoutMs;
this.maxRetries = maxRetries;
}
public HttpClient(String baseUrl) {
// Normalize before delegating
String normalizedUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
this(normalizedUrl, 5000, 3); // delegate to main constructor
}
public HttpClient(String baseUrl, int timeoutMs) {
this(baseUrl, timeoutMs, 3);
}
}
Before vs. After Summary
| Scenario | Before Java 25 | After Java 25 |
|---|---|---|
| Validate args before super() | Static helper method hack | Direct if/throw in prologue |
| Normalize args before super() | Pass raw value, normalize later | Compute in prologue, pass clean value |
| Initialize fields before super() | Not possible | Direct this.field = value |
| Throw from prologue | Only via static method side-effect | Direct throw |
Access this in prologue | Not applicable | Not allowed (no reads) |
Summary
Flexible Constructor Bodies (JEP 513) removes one of Java’s longest-standing papercuts. The prologue gives you a clean, readable place to validate and prepare data before the superclass constructor runs — no static helper methods, no factory method workarounds, no broken invariants.
The rules are simple: no reading this, no calling instance methods. Everything else is fair game.
Next up: Module Import Declarations →