Flexible Constructor Bodies
Use Java 25's flexible constructor bodies to validate and prepare arguments before calling super(...) or this(...), and know what the prologue may not touch.
Sub(Type arg) {
// prologue: validate, compute locals, assign own fields
super(prepared); // or this(...)
// epilogue: normal code, this is usable
}Until Java 25, a call to super(...) or this(...) had to be the very first statement of a constructor. Java 25 finalised flexible constructor bodies (JEP 513): statements may now come before that call. The part before it is called the prologue; the part after is the epilogue.
SavingsAccount(String id, int rate) {
if (rate < 0) throw new IllegalArgumentException("rate");
String clean = id.strip().toUpperCase();
super(clean); // the explicit constructor call
this.rate = rate;
}Why it helps
Two problems go away. First, you can fail fast: validate the arguments before the superclass constructor does any work, such as registering a handler or opening a resource. Second, you can compute arguments with ordinary statements instead of squeezing them into one expression or a static helper method like super(normalise(id)).
What the prologue may do
The object is not constructed yet, so the prologue may not use this, explicitly or implicitly. You cannot read fields, call instance methods, or use super.method(). You also cannot return from the prologue. What you can do is work with parameters and local variables, call static methods, throw exceptions, and assign fields of the class being constructed, as long as those fields have no initialiser in their declaration.
That last rule fixes an old trap. If a superclass constructor calls an overridable method, and the subclass's override reads one of its own fields, the override used to see null or 0, because the subclass fields were assigned only after super() returned. Assigning the field in the prologue means it is already set when the superclass calls back.
Mistakes to avoid
- Trying to read a field in the prologue, even one you just assigned. The compiler reports that it cannot reference the field before the supertype constructor has been called.
- Assigning in the prologue a field declared with an initialiser, such as
int retries = 3;. That is rejected; drop the initialiser and assign in each constructor. - Treating this as a reason to call overridable methods from constructors. It remains fragile; the prologue just makes the existing cases safer.
In service code this shows up in class hierarchies you do not control, such as framework base classes that demand arguments in super(...): parse configuration, check it, then hand the cleaned values up.
Example
When to use it
- Validate configuration values before a framework base class constructor registers a listener with them.
- Parse a host:port string from an environment variable, then delegate to the main constructor with this(...).
- Give a subclass field its value before a superclass constructor calls an overridden method that reads it.
More examples
Setting a field before the superclass calls back
The superclass constructor calls the overridden route() before the subclass constructor finishes. Only the version that assigns prefix in the prologue sees the real value; the old style sees the field's default of null.
abstract class Handler {
Handler() {
IO.println("Registered " + route());
}
abstract String route();
}
class OldStyleHandler extends Handler {
private final String prefix;
OldStyleHandler(String prefix) {
super();
this.prefix = prefix; // too late: route() already ran
}
String route() {
return prefix + "/users";
}
}
class UserHandler extends Handler {
private final String prefix;
UserHandler(String prefix) {
this.prefix = prefix; // allowed in the prologue: no initialiser
super();
}
String route() {
return prefix + "/users";
}
}
void main() {
new OldStyleHandler("/api/v1");
new UserHandler("/api/v1");
}
// Output:
// Registered null/users
// Registered /api/v1/usersParsing before delegating with this(...)
The string form of the address is split with normal statements, then handed to the main constructor. Before Java 25 this needed static helper methods, one for the host and one for the port, or a static factory in place of the constructor.
class Endpoint {
final String host;
final int port;
Endpoint(String host, int port) {
this.host = host;
this.port = port;
}
Endpoint(String address) {
int colon = address.lastIndexOf(':');
if (colon < 0) {
throw new IllegalArgumentException("missing port: " + address);
}
String host = address.substring(0, colon);
int port = Integer.parseInt(address.substring(colon + 1));
this(host, port);
}
}
void main() {
Endpoint db = new Endpoint("db.internal:5432");
IO.println(db.host + " / " + db.port);
try {
new Endpoint("cache.internal");
} catch (IllegalArgumentException e) {
IO.println(e.getMessage());
}
}
// Output:
// db.internal / 5432
// missing port: cache.internalThe pre-25 workaround with a static helper
Both forms still compile, so older code keeps working. The Topic version reads top to bottom, which is easier to follow once the checks grow beyond one condition.
class Resource {
final String name;
Resource(String name) {
this.name = name;
}
}
class Queue extends Resource {
Queue(String name) {
super(checked(name)); // the only option before Java 25
}
private static String checked(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("queue name is blank");
}
return "queue." + name.strip();
}
}
class Topic extends Resource {
Topic(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("topic name is blank");
}
super("topic." + name.strip());
}
}
void main() {
IO.println(new Queue(" orders ").name);
IO.println(new Topic("payments").name);
try {
new Topic(" ");
} catch (IllegalArgumentException e) {
IO.println(e.getMessage());
}
}
// Output:
// queue.orders
// topic.payments
// topic name is blank
Discussion