All articles

// Knowledge.log — 技術記事

Java 25 Lets You Validate Before super() — No Static Helper Required

JEP 513 in Java 25 lets you validate and normalize subclass arguments before super() — no static helper. Real compile, the exact javac error, JUnit 5 tests.

Through Java 24, a subclass constructor had one non-negotiable rule: super(...) (or this(...)) had to be the very first statement. If you wanted to validate or normalize an argument before handing it to the parent, there was nowhere to put that logic — it either lived in a static method called from inside the super(...) expression, or it ran after super(...), by which point the object already existed with a value that might turn out to be garbage.

Java 25 changes that rule. This piece walks through what's now legal before super(), a worked example that validates and normalizes subclass arguments with no static helper in sight, what's still off-limits (with the exact javac wording), and the JUnit tests that back up the claim. Everything here was compiled and run on this host with Temurin 25.0.4 and JUnit Jupiter 5.14.4 — no Spring, no preview flag, no made-up performance numbers.

What JEP 513 made legal

The feature is called Flexible Constructor Bodies, and it went final — not preview — in JDK 25, after three preview rounds (JEP 447 in JDK 22, JEP 482 in JDK 23, JEP 492 in JDK 24, repeated unchanged). The JEP's own summary says what matters:

> "In the body of a constructor, allow statements to appear before an explicit constructor invocation, i.e., super(...) or this(...). Such statements cannot reference the object under construction, but they can initialize its fields and perform other safe computations."

Read that second sentence twice. Statements before super(...) can't reference the object under construction, but they can initialize its fields and do other safe computation. That distinction is the whole ballgame — it's the line between "I can validate my parameters" and "I can poke at this before it's actually a thing" — and it's exactly what javac polices.

Primary source: JEP 513: Flexible Constructor Bodies.

Prologue and epilogue

The JLS calls the statements before the explicit super(...)/this(...) call the prologue, and whatever comes after the epilogue. If there's no explicit call, the prologue is empty and the compiler inserts an implicit super(); at the top — in that case everything you wrote is epilogue, same as it's always been.

Inside the prologue, the object doesn't yet exist as a valid instance of the subclass — the parent hasn't run yet. That's why the JLS calls this an "early construction context" and restricts what you can do there. See JLS SE 25, §8.8.7 Constructor Body, plus the practical write-up from Oracle: Flexible Constructor Bodies — Java SE 25.

None of this needs --enable-preview. On Java 25, javac --release 25 compiles a prologue as ordinary code — it's a shipped feature, not a preview.

Validating and normalizing before super(), no static helper

The example is the classic money-in-cents setup: a Money class holding a long cents, and a PositiveMoney subclass that only accepts positive values — and that also knows how to parse a string like " $1_000 " into 1000 cents. Before JEP 513, both the validation and the string normalization had to live in a static method so they could be squeezed into the super(...) expression. Now they live directly in the constructor, as plain sequential code.

Money, nothing new here:

package money;

public class Money {
    private final long cents;

    public Money(long cents) {
        this.cents = cents;
    }

    public long cents() {
        return cents;
    }
}

PositiveMoney, with two constructors that validate before calling super(cents) — no static helper anywhere:

package money;

import java.util.Objects;

public class PositiveMoney extends Money {
    public PositiveMoney(Long cents) {
        Objects.requireNonNull(cents, "cents");
        if (cents <= 0) {
            throw new IllegalArgumentException("cents must be > 0, got " + cents);
        }
        super(cents);
    }

    public PositiveMoney(String raw) {
        Objects.requireNonNull(raw, "raw");
        String normalized = raw.strip().replace("_", "");
        if (normalized.startsWith("$")) {
            normalized = normalized.substring(1);
        }
        long cents = Long.parseLong(normalized);
        if (cents <= 0) {
            throw new IllegalArgumentException("cents must be > 0, got " + cents);
        }
        super(cents);
    }
}

Look closely at what each constructor is doing. Objects.requireNonNull is a static call, allowed in the prologue because it never touches the object under construction. The range check (cents <= 0) only reads the local parameter, not the instance. The string normalization (strip, replace, substring, parseLong) also operates purely on local variables. None of these lines reference this, none call an instance method, none read a field of the object — which is precisely why the compiler is fine with all of it happening before super(cents).

Compiling with the real compiler, no preview flag:

$ javac --release 25 -d ok/out ok/Money.java ok/PositiveMoney.java

Output: clean compile, exit code 0. No preview flag, no warning.

Oracle's own guide uses a sibling example — a PositiveBigInteger that checks value <= 0 and then calls super(Long.toString(value)) — following the exact same "validate, normalize, then super" pattern. Worth the extra read: Flexible Constructor Bodies — Java SE 25.

If you've watched a team scatter verify* static methods around purely so they could squeeze them inside the parentheses of super(...), this is the pattern JEP 513 retires. The static helper still compiles fine — it just isn't load-bearing anymore.

What's still forbidden — and the exact javac error

The prologue got room to validate and compute, not license to act like the object already exists. Still forbidden before super(...)/this(...):

  • Reading this (except the narrow case of assigning a field with no initializer).
  • Reading or writing instance fields that already have an initializer in their declaration.
  • Calling an instance method (even implicitly, with no this. in front).
  • Accessing super.field or calling super.method().
  • Using return, or wrapping the super(...)/this(...) call itself in a try.

A constructor that trips over nearly all of those at once:

class Super {
    int j;
    void m() {}
}

class IllegalThis extends Super {
    int i;
    String s = "hello";

    IllegalThis() {
        this.i++;          // 1
        i++;               // 2
        this.hashCode();   // 3
        hashCode();        // 4
        System.out.print(this); // 5
        super.j++;         // 6
        super.m();         // 7
        s = "goodbye";     // 8 assigning an already-initialized field
        super();
    }
}

Compiling with javac --release 25 fail/IllegalThis.java, the host hands back 9 errors, including:

cannot reference this before supertype constructor has been called
cannot reference i before supertype constructor has been called
cannot reference hashCode() before supertype constructor has been called
cannot reference super before supertype constructor has been called
cannot assign initialized field 's' before supertype constructor has been called

Each line in the example maps to one of these diagnostics: this.i++ and i++ trigger "cannot reference this / i"; this.hashCode() and hashCode() trigger the method variant; super.j++ and super.m() trigger "cannot reference super"; and s = "goodbye" — assigning a field that already has an initializer (String s = "hello") — triggers the assignment error, which is a different diagnostic from the read error. Easy detail to lose track of: a field without an initializer can be assigned in the prologue; a field with one can't.

Fix: move every read of this, every instance-method call, and every super.* access to after super(...) — that is, into the epilogue, where it's always been fine. Production note: this gate needs no runtime test because it's a compile-time error — javac --release 25 in CI already blocks the commit before anything ships. It's the same flavor of guarantee as the article on sealed interfaces instead of boolean flag soup (also up in English, as sealed interfaces instead of boolean flag soup), which uses an exhaustive switch over a sealed interface to keep invalid states unrepresentable — same category of error, different mechanism: there it's the switch, here it's the constructor prologue.

JUnit 5 tests for the happy path and the rejections

Compiling without errors proves the syntax is legal; it doesn't prove the validation does what you think it does. For that, five JUnit Jupiter tests, all plain Java SE — no Spring, no Testcontainers:

import org.junit.jupiter.api.Test;
import money.PositiveMoney;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

class PositiveMoneyTest {

    @Test
    void happyPathStoresCents() {
        assertEquals(250, new PositiveMoney(250L).cents());
    }

    @Test
    void normalizesCurrencyAndSeparatorsBeforeSuper() {
        assertEquals(1000, new PositiveMoney(" $1_000 ").cents());
    }

    @Test
    void rejectsNullCents() {
        assertThrows(NullPointerException.class, () -> new PositiveMoney((Long) null));
    }

    @Test
    void rejectsNonPositiveCents() {
        assertThrows(IllegalArgumentException.class, () -> new PositiveMoney(0L));
        assertThrows(IllegalArgumentException.class, () -> new PositiveMoney(-1L));
    }

    @Test
    void rejectsNullRaw() {
        assertThrows(NullPointerException.class, () -> new PositiveMoney((String) null));
    }
}

Running it without Maven, straight from the JUnit Platform console launcher:

javac --release 25 -cp junit-platform-console-standalone-1.14.4.jar:ok/out \
  -d tests/out tests/src/PositiveMoneyTest.java

java --class-path junit-platform-console-standalone-1.14.4.jar:ok/out:tests/out \
  org.junit.platform.console.ConsoleLauncher --select-class PositiveMoneyTest

Result on this host, 2026-09-20: 5 tests started, 5 successful, 0 failed — happyPathStoresCents, normalizesCurrencyAndSeparatorsBeforeSuper, rejectsNullCents, rejectsNonPositiveCents, rejectsNullRaw. Concretely: new PositiveMoney(250L).cents() returns 250; new PositiveMoney(" $1_000 ").cents() returns 1000 — the dollar sign, space, and underscore all get stripped in the prologue, before super(cents) ever sees the final value. A null Long throws NullPointerException; 0L or -1L throws IllegalArgumentException; a null String also throws NullPointerException.

If you'd rather use Maven than run javac/java by hand, the pom.xml stays small — this is plain Java SE, no framework plugin involved:

<properties>
  <maven.compiler.release>25</maven.compiler.release>
  <junit.version>5.14.4</junit.version>
</properties>

<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>5.14.4</version>
  <scope>test</scope>
</dependency>

Docs for the version used: JUnit 5 User Guide (5.14.4). Staying on JUnit Jupiter 5.14.4 here isn't because 6.x breaks on Java 25 — it's that this example is built on the 5.x line, and mixing the two would add noise the article doesn't need.

Fix when a rejection test fails: check that the exception declared in assertThrows matches what the constructor actually throws — NullPointerException for a null argument, IllegalArgumentException for an out-of-range value; they're different types, and swapping one for the other means the test is aiming at the wrong thing. Production note: the rejection tests are the safety net for the business rule (value <= 0, null string); javac covers the syntax of the prologue, the tests cover the semantics of the validation. Skip either one and you've got a gap.

Recommendation

Within this stack — Java 25 Temurin, JUnit Jupiter 5.14.4, no framework layered on top — swapping a static helper for direct validation in the constructor prologue pays off whenever a subclass needs to reject or normalize an argument before handing it to the parent. It's less indirection: the invariant reads top-to-bottom inside the constructor itself, instead of being split between the super(...) call signature and a static method that only exists to appease an older compiler.

Two cases where it's worth backing off: if the project still needs to compile on a pre-25 JDK (the prologue simply doesn't exist there, and no flag brings it back), or if the validation logic is complex enough to deserve its own object — at that point, the win of moving logic into the constructor loses out to the win of naming the rule as something independently testable. For the common case — requireNonNull, a range check, a bit of string normalization — the constructor already handles it, and javac --release 25 remains the gate that flags anyone trying to read the instance before it's actually there.

javajdk-25

// Continue.training — 次のステップ

Knowledge only counts when it becomes practice.

Go back to the article, run the examples, and share what you learned.

Explore more articles