How do I Validate Arguments Before Calling super() in Java 25?

Java 25 includes JEP 513: Flexible Constructor Bodies (finalized in Java 25 after being previewed in JEP 447, 482, and 492). This feature allows you to execute statements — including argument validation — before invoking super(...) or this(...), as long as those statements don’t access the instance being constructed.

The Old Problem (Pre-Java 22)

Before flexible constructor bodies, super(...) or this(...) had to be the first statement in a constructor. This forced awkward workarounds:

public class PositiveBigInteger extends BigInteger {
    public PositiveBigInteger(long value) {
        super(String.valueOf(value)); // must be first!
        if (value <= 0) {
            throw new IllegalArgumentException("Value must be positive");
        }
        // Superclass was already constructed with invalid data
    }
}

Workarounds required static helper methods:

public PositiveBigInteger(long value) {
    super(validate(value));
}
private static String validate(long value) {
    if (value <= 0) throw new IllegalArgumentException("Value must be positive");
    return String.valueOf(value);
}

The Java 25 Way — Prologue Code

You can now write a prologue — statements before super(...) — that validates, transforms, or prepares arguments:

public class PositiveBigInteger extends BigInteger {
    public PositiveBigInteger(long value) {
        // Prologue: fail fast BEFORE constructing the superclass
        if (value <= 0) {
            throw new IllegalArgumentException("Value must be positive, got: " + value);
        }
        super(String.valueOf(value));
        // Epilogue: normal constructor body
    }
}

Practical Examples

1. Validation with Transformation

public class EmailAddress {
    private final String normalized;

    public EmailAddress(String raw) {
        // Validate & normalize BEFORE any field assignment or super() call
        Objects.requireNonNull(raw, "raw email must not be null");
        String trimmed = raw.trim().toLowerCase();
        if (!trimmed.contains("@")) {
            throw new IllegalArgumentException("Invalid email: " + raw);
        }
        this.normalized = trimmed; // field assignment is allowed in prologue
    }
}

2. Sharing Expensive Computation Between this() Chains

public class Matrix {
    public Matrix(double[][] data) {
        // Validate once, use twice
        if (data == null || data.length == 0) {
            throw new IllegalArgumentException("Matrix cannot be empty");
        }
        int rows = data.length;
        int cols = data[0].length;
        this(rows, cols, flatten(data));
    }

    public Matrix(int rows, int cols, double[] flat) { /* ... */ }
}

3. Conditional Superclass Arguments

public class LoggingList<E> extends ArrayList<E> {
    public LoggingList(Collection<? extends E> source) {
        // Compute capacity hint before calling super
        int capacity = (source == null) ? 10 : Math.max(16, source.size() * 2);
        super(capacity);
        if (source != null) addAll(source);
    }
}

Rules You Must Follow

The prologue code runs before the instance is fully initialized, so the compiler enforces strict rules:

Allowed in prologue Not allowed in prologue
Reading/writing local variables Reading this.field
Assigning to own instance fields (this.x = ...) Calling instance methods on this
Throwing exceptions Using this as an expression
Calling static methods Referring to super.field
Using constructor parameters Reading fields declared in the superclass
public class Example extends Parent {
    private final int value;

    public Example(int input) {
        if (input < 0) throw new IllegalArgumentException(); // OK
        this.value = input * 2;                              // OK (own field write)
        // System.out.println(this.value);                   // Can't READ own field
        // someInstanceMethod();                             // Can't call instance method
        super(computeParentArg(input));                      // static call is fine
    }

    private static int computeParentArg(int x) { return x + 1; }
}

Key Benefits

  1. Fail fast — reject invalid arguments before allocating superclass state
  2. No more static helper methods just to satisfy the “super must be first” rule
  3. Cleaner code — validation lives next to the constructor that needs it
  4. Better performance — avoid partially constructing objects that will be discarded
  5. DRY — share computation between multiple this(...) delegations

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.