Add jep513 module: flexible constructor bodies on JDK 25 and 27

Runnable sources, broken examples and captured transcripts for the ankurm.com JEP 513 article, including the JDK 25 vs 27 compiler wording differences.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a
This commit is contained in:
Claude
2026-09-24 10:42:36 +00:00
parent e0cb6e83e2
commit 2ebea58d39
30 changed files with 510 additions and 0 deletions
+1
View File
@@ -63,6 +63,7 @@ These modules keep only sources, a `run.sh` and their transcripts (`output/`). T
| Module | Article | What it demonstrates | | Module | Article | What it demonstrates |
|---|---|---| |---|---|---|
| [`jep512/`](jep512) | Compact Source Files and Instance Main Methods in Java 25 (JEP 512) | `java Hello.java` on JDK 21 vs 25 vs 27, the class `javac` builds around `void main()`, implicit `java.base` imports, `java.lang.IO`, the measured launch order, a private-constructor difference between 25 and 27 | | [`jep512/`](jep512) | Compact Source Files and Instance Main Methods in Java 25 (JEP 512) | `java Hello.java` on JDK 21 vs 25 vs 27, the class `javac` builds around `void main()`, implicit `java.base` imports, `java.lang.IO`, the measured launch order, a private-constructor difference between 25 and 27 |
| [`jep513/`](jep513) | Flexible Constructor Bodies in Java 25 (JEP 513): Validate Before super() | statements before `super(...)`/`this(...)`, the parent-calls-child bug and its fix, twelve compile errors that remain with the 25 and 27 wordings side by side, `--release` rejection |
## Captured output (`docs/output/`) ## Captured output (`docs/output/`)
+11
View File
@@ -0,0 +1,11 @@
# jep513 — Flexible Constructor Bodies (JEP 513, final in Java 25)
Companion code for the ankurm.com article **Flexible Constructor Bodies in Java 25 (JEP 513): Validate Before super()**. The explanation
lives in the article; this folder holds the runnable sources and the transcripts they produced.
```bash
JDK25=/path/to/jdk-25 JDK27=/path/to/jdk-27 [JDK21=/path/to/jdk-21] ./run.sh # regenerates output/*.txt
```
`src/` runs; `broken/` fails to compile on purpose (the messages are captured in `output/03`, `07` and `08`). Tested on Temurin 25.0.4.1+1 and 27+35;
JDK 21 is used only to show the "before" error.
@@ -0,0 +1,8 @@
class Parent { Parent() {} }
class AssignFieldWithInitializer extends Parent {
int size = 10; // has an initializer, which runs AFTER super()
AssignFieldWithInitializer() {
this.size = 20; // assignment in the prologue
super();
}
}
+7
View File
@@ -0,0 +1,7 @@
class Parent { int shared; Parent() {} }
class AssignInheritedField extends Parent {
AssignInheritedField() {
this.shared = 7; // a field the SUPERCLASS declares
super();
}
}
+8
View File
@@ -0,0 +1,8 @@
class Parent { Parent(int x) {} }
class CallMethodBeforeSuper extends Parent {
int compute() { return 42; }
CallMethodBeforeSuper() {
int v = compute(); // instance method call before super()
super(v);
}
}
+7
View File
@@ -0,0 +1,7 @@
class Base { Base(Runnable r) {} }
class InnerOuterThis extends Base {
int x = 3;
InnerOuterThis() {
super(() -> IO.println(x)); // lambda capturing this.x inside the super(...) arguments
}
}
+8
View File
@@ -0,0 +1,8 @@
class Parent { Parent(Object o) {} }
class NewInnerBeforeSuper extends Parent {
class Inner {}
NewInnerBeforeSuper() {
Inner i = new Inner(); // an inner class instance needs an enclosing this
super(i);
}
}
+14
View File
@@ -0,0 +1,14 @@
// Written with System.out so that JDK 21 can compile it: the ONLY thing wrong with it on 21 is the statement before super(...).
public class OnJdk21 {
static class Person { Person(String n) {} }
static class Employee extends Person {
Employee(String name, int salary) {
if (salary < 0) throw new IllegalArgumentException("negative");
super(name.strip());
}
}
public static void main(String[] args) {
new Employee(" a ", 1);
System.out.println("constructed");
}
}
+7
View File
@@ -0,0 +1,7 @@
class Parent { Parent(Object o) {} }
class PassThisBeforeSuper extends Parent {
PassThisBeforeSuper() {
Object self = this; // 'this' escapes before super()
super(self);
}
}
+8
View File
@@ -0,0 +1,8 @@
class Parent { Parent(int x) {} }
class ReadFieldBeforeSuper extends Parent {
int size = 10;
ReadFieldBeforeSuper() {
int copy = size; // reads this.size before super()
super(copy);
}
}
+9
View File
@@ -0,0 +1,9 @@
class Parent { Parent() {} }
class ReadFinalBeforeAssign extends Parent {
final String label;
ReadFinalBeforeAssign(String s) {
String t = this.label; // reading a field, even a final one, in the prologue
this.label = s;
super();
}
}
+7
View File
@@ -0,0 +1,7 @@
class Parent { Parent(int x) {} }
class ReturnBeforeSuper extends Parent {
ReturnBeforeSuper(int x) {
if (x < 0) return; // cannot return before the super call has completed
super(x);
}
}
+6
View File
@@ -0,0 +1,6 @@
class Parent { Parent(int x) {} }
class SuperInBranch extends Parent {
SuperInBranch(boolean big) {
if (big) super(100); else super(1); // super(...) must appear once, at top level of the constructor body
}
}
+7
View File
@@ -0,0 +1,7 @@
class Parent { Parent(int x) {} Parent() {} }
class TwoSuperCalls extends Parent {
TwoSuperCalls() {
super(1);
super(); // a second explicit constructor call
}
}
@@ -0,0 +1,13 @@
$ javac src/ValidateBeforeSuper.java && java ValidateBeforeSuper (OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
valid input:
Person constructor ran for 'Asha'
name='Asha' salary=50000
invalid input:
caught: salary must be >= 0, got -1 (note: no 'Person constructor ran' line above)
$ javac src/ValidateBeforeSuper.java && java ValidateBeforeSuper (OpenJDK Runtime Environment Temurin-27+35 (build 27+35))
valid input:
Person constructor ran for 'Asha'
name='Asha' salary=50000
invalid input:
caught: salary must be >= 0, got -1 (note: no 'Person constructor ran' line above)
+9
View File
@@ -0,0 +1,9 @@
$ javac src/BeforeJava25.java && java BeforeJava25 (OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
Person constructor ran for 'Asha'
name='Asha' salary=50000
caught: salary must be >= 0, got -1
$ javac src/BeforeJava25.java && java BeforeJava25 (OpenJDK Runtime Environment (build 21.0.10+7-Ubuntu-124.04))
Person constructor ran for 'Asha'
name='Asha' salary=50000
caught: salary must be >= 0, got -1
@@ -0,0 +1,11 @@
$ javac broken/OnJdk21.java && java OnJdk21 (OpenJDK Runtime Environment (build 21.0.10+7-Ubuntu-124.04))
broken/OnJdk21.java:7: error: call to super must be first statement in constructor
super(name.strip());
^
1 error
$ javac broken/OnJdk21.java && java OnJdk21 (OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
constructed
$ javac broken/OnJdk21.java && java OnJdk21 (OpenJDK Runtime Environment Temurin-27+35 (build 27+35))
constructed
@@ -0,0 +1,5 @@
$ javac src/OverridableCall.java && java OverridableCall (OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
Broken (assign after super()):
Base constructor calls describe(): label=null
Fixed (assign before super()):
Base constructor calls describe(): label=hello
+11
View File
@@ -0,0 +1,11 @@
$ javac src/ParseThenSuper.java && java ParseThenSuper (OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
design review at 09:05
bad input failed before Time was built: NumberFormatException
$ javac src/TryInPrologue.java && java TryInPrologue (OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
configured '9090' -> port 9090
configured 'oops' -> port 8080
$ javac src/ThisChaining.java && java ThisChaining (OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
critical-payment retries=5
report-export retries=1
+13
View File
@@ -0,0 +1,13 @@
$ javap -c -p 'PrologueBytecode$Child' (OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
class PrologueBytecode$Child extends PrologueBytecode$Parent {
PrologueBytecode$Child(int);
Code:
0: iload_1
1: iconst_0
2: invokestatic #1 // Method java/lang/Math.max:(II)I
5: istore_2
6: aload_0
7: iload_2
8: invokespecial #7 // Method PrologueBytecode$Parent."<init>":(I)V
11: return
}
@@ -0,0 +1,79 @@
$ javac broken/ReadFieldBeforeSuper.java (25.0.4.1+1)
ReadFieldBeforeSuper.java:5: error: cannot reference size before supertype constructor has been called
1 error
$ javac broken/ReadFieldBeforeSuper.java (27+35)
ReadFieldBeforeSuper.java:5: error: reference to size may only appear after an explicit constructor invocation
1 error
$ javac broken/CallMethodBeforeSuper.java (25.0.4.1+1)
CallMethodBeforeSuper.java:5: error: cannot reference compute() before supertype constructor has been called
1 error
$ javac broken/CallMethodBeforeSuper.java (27+35)
CallMethodBeforeSuper.java:5: error: reference to compute() may only appear after an explicit constructor invocation
1 error
$ javac broken/PassThisBeforeSuper.java (25.0.4.1+1)
PassThisBeforeSuper.java:4: error: cannot reference this before supertype constructor has been called
1 error
$ javac broken/PassThisBeforeSuper.java (27+35)
PassThisBeforeSuper.java:4: error: reference to this may only appear after an explicit constructor invocation
1 error
$ javac broken/ReadFinalBeforeAssign.java (25.0.4.1+1)
ReadFinalBeforeAssign.java:5: error: cannot reference this before supertype constructor has been called
1 error
$ javac broken/ReadFinalBeforeAssign.java (27+35)
ReadFinalBeforeAssign.java:5: error: reference to this may only appear after an explicit constructor invocation
1 error
$ javac broken/InnerOuterThis.java (25.0.4.1+1)
InnerOuterThis.java:5: error: cannot reference x before supertype constructor has been called
1 error
$ javac broken/InnerOuterThis.java (27+35)
InnerOuterThis.java:5: error: reference to x may only appear after an explicit constructor invocation
1 error
$ javac broken/NewInnerBeforeSuper.java (25.0.4.1+1)
NewInnerBeforeSuper.java:5: error: cannot reference this before supertype constructor has been called
1 error
$ javac broken/NewInnerBeforeSuper.java (27+35)
NewInnerBeforeSuper.java:5: error: reference to this may only appear after an explicit constructor invocation
1 error
$ javac broken/AssignFieldWithInitializer.java (25.0.4.1+1)
AssignFieldWithInitializer.java:5: error: cannot assign initialized field 'size' before supertype constructor has been called
1 error
$ javac broken/AssignFieldWithInitializer.java (27+35)
AssignFieldWithInitializer.java:5: error: assignment to initialized field 'size' may only appear after an explicit constructor invocation
1 error
$ javac broken/AssignInheritedField.java (25.0.4.1+1)
AssignInheritedField.java:4: error: cannot reference shared before supertype constructor has been called
1 error
$ javac broken/AssignInheritedField.java (27+35)
AssignInheritedField.java:4: error: reference to shared may only appear after an explicit constructor invocation
1 error
$ javac broken/ReturnBeforeSuper.java (25.0.4.1+1)
ReturnBeforeSuper.java:4: error: 'return' not allowed before explicit constructor invocation
1 error
$ javac broken/ReturnBeforeSuper.java (27+35)
ReturnBeforeSuper.java:4: error: 'return' not allowed before explicit constructor invocation
1 error
$ javac broken/SuperInBranch.java (25.0.4.1+1)
SuperInBranch.java:4: error: explicit constructor invocation not allowed here
SuperInBranch.java:4: error: explicit constructor invocation not allowed here
2 errors
$ javac broken/SuperInBranch.java (27+35)
SuperInBranch.java:4: error: explicit constructor invocation not allowed here
SuperInBranch.java:4: error: explicit constructor invocation not allowed here
2 errors
$ javac broken/TwoSuperCalls.java (25.0.4.1+1)
TwoSuperCalls.java:5: error: redundant explicit constructor invocation
1 error
$ javac broken/TwoSuperCalls.java (27+35)
TwoSuperCalls.java:5: error: redundant explicit constructor invocation
1 error
+19
View File
@@ -0,0 +1,19 @@
$ javac --release 21 broken/OnJdk21.java (25.0.4.1+1)
OnJdk21.java:7: error: flexible constructors is not supported in -source 21
super(name.strip());
^
(use -source 25 or higher to enable flexible constructors)
1 error
exit=1
$ javac --release 24 broken/OnJdk21.java (25.0.4.1+1)
OnJdk21.java:7: error: flexible constructors is not supported in -source 24
super(name.strip());
^
(use -source 25 or higher to enable flexible constructors)
1 error
exit=1
$ javac --release 25 broken/OnJdk21.java (25.0.4.1+1)
exit=0
Executable
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# JEP 513 (Flexible Constructor Bodies): final in JDK 25, no --enable-preview.
# Regenerates every file in output/. JDK25=/path JDK27=/path [JDK21=/path] ./run.sh
set -uo pipefail
unset JAVA_TOOL_OPTIONS
HERE="$(cd "$(dirname "$0")" && pwd)"
JDK25="${JDK25:-/opt/jdks/jdk-25.0.4.1+1}"; JDK27="${JDK27:-/opt/jdks/jdk27}"; JDK21="${JDK21:-}"
OUT="$HERE/output"; mkdir -p "$OUT"; B="$(mktemp -d)"; trap 'rm -rf "$B"' EXIT
cd "$HERE"
v() { "$1/bin/java" -version 2>&1 | sed -n 2p; }
# compile SRC with JDK, then run CLASS from it; prints a header naming the JDK
cr() { local jdk="$1" src="$2" cls="$3"; local o="$B/$(basename "$jdk")-$cls"; mkdir -p "$o"
echo "\$ javac $src && java $cls ($(v "$jdk"))"
"$jdk/bin/javac" -d "$o" "$src" 2>&1 && "$jdk/bin/java" -cp "$o" "$cls" 2>&1; }
{ cr "$JDK25" src/ValidateBeforeSuper.java ValidateBeforeSuper; echo; cr "$JDK27" src/ValidateBeforeSuper.java ValidateBeforeSuper
} > "$OUT/01-validate-before-super.txt"
{ cr "$JDK25" src/BeforeJava25.java BeforeJava25
[ -n "$JDK21" ] && { echo; cr "$JDK21" src/BeforeJava25.java BeforeJava25; }
} > "$OUT/02-the-old-workaround.txt"
{ [ -n "$JDK21" ] && { cr "$JDK21" broken/OnJdk21.java OnJdk21; echo; }
cr "$JDK25" broken/OnJdk21.java OnJdk21; echo; cr "$JDK27" broken/OnJdk21.java OnJdk21
} > "$OUT/03-same-file-on-21-25-27.txt"
{ cr "$JDK25" src/OverridableCall.java OverridableCall; } > "$OUT/04-overridable-call-bug-and-fix.txt"
{ cr "$JDK25" src/ParseThenSuper.java ParseThenSuper; echo; cr "$JDK25" src/TryInPrologue.java TryInPrologue; echo; cr "$JDK25" src/ThisChaining.java ThisChaining
} > "$OUT/05-prologue-in-practice.txt"
{ o="$B/bc"; mkdir -p "$o"; "$JDK25/bin/javac" -d "$o" src/PrologueBytecode.java 2>&1
echo "\$ javap -c -p 'PrologueBytecode\$Child' ($(v "$JDK25"))"; "$JDK25/bin/javap" -c -p -cp "$o" 'PrologueBytecode$Child' 2>&1 | sed -n '2,$p'
} > "$OUT/06-bytecode.txt"
# the rules that did not go away: same source, both JDKs, because the wording changed between 25 and 27
{ for f in ReadFieldBeforeSuper CallMethodBeforeSuper PassThisBeforeSuper ReadFinalBeforeAssign InnerOuterThis NewInnerBeforeSuper AssignFieldWithInitializer AssignInheritedField ReturnBeforeSuper SuperInBranch TwoSuperCalls; do
for j in "$JDK25" "$JDK27"; do
echo "\$ javac broken/$f.java ($(v "$j" | sed 's/.*Temurin-//;s/ (build.*//'))"
"$j/bin/javac" -d "$B/x" broken/$f.java 2>&1 | grep -E 'error|warning' | sed 's/^broken\///'
done; echo
done
} > "$OUT/07-still-illegal-25-vs-27.txt"
# 08 - the language level matters: compiling this source for an older target from JDK 25
{ for r in 21 24 25; do echo "\$ javac --release $r broken/OnJdk21.java ($(v "$JDK25" | sed 's/.*Temurin-//;s/ (build.*//'))"
"$JDK25/bin/javac" --release $r -d "$B/r$r" broken/OnJdk21.java 2>&1 | grep -v '^Note:' | sed 's/^broken\///'; echo "exit=${PIPESTATUS[0]}"; echo; done
} > "$OUT/08-release-levels.txt"
echo "wrote $OUT"
+35
View File
@@ -0,0 +1,35 @@
// The same validation the way it had to be written before Java 25: hide it inside the super(...) argument
// via a static helper. This compiles on every JDK from 8 up, including 21 (so it uses System.out, not IO).
public class BeforeJava25 {
static class Person {
final String name;
Person(String name) {
this.name = name;
System.out.println(" Person constructor ran for '" + name + "'");
}
}
static class Employee extends Person {
final int salary;
Employee(String name, int salary) {
super(checkAndClean(name, salary)); // validation smuggled into an argument expression
this.salary = salary;
}
private static String checkAndClean(String name, int salary) {
if (salary < 0) throw new IllegalArgumentException("salary must be >= 0, got " + salary);
return name.strip();
}
}
public static void main(String[] args) {
Employee e = new Employee(" Asha ", 50_000);
System.out.println(" name='" + e.name + "' salary=" + e.salary);
try {
new Employee("Ravi", -1);
} catch (IllegalArgumentException ex) {
System.out.println(" caught: " + ex.getMessage());
}
}
}
+36
View File
@@ -0,0 +1,36 @@
// The classic superclass-constructor trap, and the fix JEP 513 makes possible.
public class OverridableCall {
static class Base {
Base() {
IO.println(" Base constructor calls describe(): " + describe());
}
String describe() { return "base"; }
}
// The bug: describe() runs from Base's constructor, before Broken's own field has been assigned.
static class Broken extends Base {
private final String label;
Broken(String label) {
super();
this.label = label;
}
@Override String describe() { return "label=" + label; }
}
// The fix: assign the field in the prologue, before super() runs Base's constructor.
static class Fixed extends Base {
private final String label;
Fixed(String label) {
this.label = label; // allowed in the prologue: a plain assignment to a field of THIS class
super();
}
@Override String describe() { return "label=" + label; }
}
public static void main(String[] args) {
IO.println("Broken (assign after super()):");
new Broken("hello");
IO.println("Fixed (assign before super()):");
new Fixed("hello");
}
}
+30
View File
@@ -0,0 +1,30 @@
// Turning one constructor argument into two for the superclass: previously this needed a static factory
// or a private constructor chain. Now it is straight-line code before super(...).
public class ParseThenSuper {
static class Time {
final int hours, minutes;
Time(int hours, int minutes) { this.hours = hours; this.minutes = minutes; }
@Override public String toString() { return String.format("%02d:%02d", hours, minutes); }
}
static class Meeting extends Time {
final String title;
Meeting(String title, String at) {
String[] parts = at.split(":");
int h = Integer.parseInt(parts[0]);
int m = Integer.parseInt(parts[1]);
super(h, m);
this.title = title;
}
}
public static void main(String[] args) {
Meeting m = new Meeting("design review", "9:05");
IO.println(m.title + " at " + m);
try {
new Meeting("bad", "nine-oh-five");
} catch (RuntimeException ex) {
IO.println("bad input failed before Time was built: " + ex.getClass().getSimpleName());
}
}
}
+11
View File
@@ -0,0 +1,11 @@
// The JVM never had the "super() must be first" rule; only the Java language did. Look at the bytecode.
public class PrologueBytecode {
static class Parent { Parent(int x) {} }
static class Child extends Parent {
Child(int raw) {
int checked = Math.max(raw, 0); // ordinary statement BEFORE the super call
super(checked);
}
}
public static void main(String[] args) { new Child(-5); IO.println("ran"); }
}
+23
View File
@@ -0,0 +1,23 @@
// The prologue rule applies to this(...) as well as super(...).
public class ThisChaining {
final String id;
final int retries;
ThisChaining(String id) {
String normalised = id.trim().toLowerCase();
int defaultRetries = normalised.startsWith("critical") ? 5 : 1;
this(normalised, defaultRetries);
}
ThisChaining(String id, int retries) {
this.id = id;
this.retries = retries;
}
public static void main(String[] args) {
ThisChaining a = new ThisChaining(" CRITICAL-payment ");
ThisChaining b = new ThisChaining("Report-export");
IO.println(a.id + " retries=" + a.retries);
IO.println(b.id + " retries=" + b.retries);
}
}
+24
View File
@@ -0,0 +1,24 @@
// The prologue is ordinary code: try/catch, loops and local variables all work, as long as they leave 'this' alone.
public class TryInPrologue {
static class Port {
final int number;
Port(int number) { this.number = number; }
}
static class Server extends Port {
Server(String configured) {
int parsed;
try {
parsed = Integer.parseInt(configured);
} catch (NumberFormatException e) {
parsed = 8080; // fall back instead of failing
}
super(parsed);
}
}
public static void main(String[] args) {
IO.println("configured '9090' -> port " + new Server("9090").number);
IO.println("configured 'oops' -> port " + new Server("oops").number);
}
}
+35
View File
@@ -0,0 +1,35 @@
// JEP 513: statements may run BEFORE super(...), as long as they do not touch the object under construction.
public class ValidateBeforeSuper {
static class Person {
final String name;
Person(String name) {
this.name = name;
IO.println(" Person constructor ran for '" + name + "'");
}
}
static class Employee extends Person {
final int salary;
Employee(String name, int salary) {
// Validate and tidy first. If this throws, Person's constructor never runs.
if (salary < 0) throw new IllegalArgumentException("salary must be >= 0, got " + salary);
String cleaned = name.strip();
super(cleaned);
this.salary = salary;
}
}
public static void main(String[] args) {
IO.println("valid input:");
Employee e = new Employee(" Asha ", 50_000);
IO.println(" name='" + e.name + "' salary=" + e.salary);
IO.println("invalid input:");
try {
new Employee("Ravi", -1);
} catch (IllegalArgumentException ex) {
IO.println(" caught: " + ex.getMessage() + " (note: no 'Person constructor ran' line above)");
}
}
}