Part 3 of 12

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(...) or this(...) 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

ScenarioBefore Java 25After Java 25
Validate args before super()Static helper method hackDirect if/throw in prologue
Normalize args before super()Pass raw value, normalize laterCompute in prologue, pass clean value
Initialize fields before super()Not possibleDirect this.field = value
Throw from prologueOnly via static method side-effectDirect throw
Access this in prologueNot applicableNot 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 →