QUICKREF · LANGUAGE · JEP 513
Flexible Constructor Bodies:把校验放到 super() 之前
JDK 25 · 速查主题 05 / 06 · 预计 25 分钟
一句话JDK 25 正式允许在
super(...) / this(...) 之前写语句:可以先校验参数、计算参数、初始化本类字段,再调用父构造器。代价是这些语句处于「早期构造上下文」,不能读取正在构造的对象。学完你要能自己回答
- super() 之前可以写哪些语句?哪些操作会被编译器拒绝?
- 为什么允许给字段赋值,却不允许读字段?字段有初始化器 时为什么不行?
- prologue 与 epilogue 的执行顺序是什么?这对父构造器调用可覆写方法有什么意义?
旧写法:校验只能塞进 super() 参数01
在 JDK 25 之前,构造器体必须以 super(...) 或 this(...) 开头。想在调用父构造器前校验参数,只能把逻辑塞进参数表达式或静态辅助方法,失败时的堆栈和可读性都不理想。
// 旧写法:校验藏在参数里
Employee(int age, String officeId) {
super(validateAge(age), officeId);
this.officeId = officeId;
}
static int validateAge(int age) {
if (age < 18 || age > 67) {
throw new IllegalArgumentException("age out of range");
}
return age;
}
JEP 513 把这部分语句称为构造器的 prologue:它在显式构造器调用之前执行,可以 fail fast,也可以先初始化本类字段。显式调用之后的代码是 epilogue。
// JDK 25:校验直接写在 super() 之前
Employee(int age, String officeId) {
if (age < 18 || age > 67) {
throw new IllegalArgumentException("age out of range");
}
super(age);
this.officeId = officeId;
}
- prologue 可以包含局部变量、条件、try/catch、throw 等不依赖当前实例的语句。
super(...)/this(...)仍然必须恰好出现一次。- prologue 执行完才调用父构造器;父构造器返回后才进入 epilogue。
这不是「允许在 super 前随便用 this」。早期构造上下文的限制很严格,下一节逐条列。
早期构造上下文:能用什么,不能用什么02
prologue 和显式构造器调用的参数列表都属于早期构造上下文(early construction context)。代码不能使用正在构造的实例,只能使用参数、局部变量、静态成员,以及给本类未初始化的字段赋值。
| 操作 | 允许? | 说明 |
|---|---|---|
| 使用构造器参数、局部变量 | 允许 | 普通表达式 |
| 调用静态方法 / 访问静态字段 | 允许 | 不依赖实例 |
| 给本类无初始化器 的字段赋值 | 允许 | simple assignment;含 final 字段一次赋值 |
| 给有初始化器 的字段赋值 | 禁止 | 编译器报 cannot assign initialized field |
| 读取本类字段 | 禁止 | 隐式或显式 this 都不行 |
| 调用本类实例方法 / 使用 this | 禁止 | 不能把 this 传出去 |
| 在 prologue 里 return | 禁止 | epilogue 可以 return;(不带值) |
| throw | 允许 | fail fast 的典型用法 |
class Base {
Base(int x) {}
}
public class BeforeSuperRead extends Base {
private int value;
BeforeSuperRead(int x) {
System.out.println(value); // 读字段
super(x);
}
}
BeforeSuperRead.java:9: error: cannot reference value before supertype constructor has been called
System.out.println(value);
^
1 error
class Base3 {
Base3(int x) {}
}
public class BeforeSuperThis extends Base3 {
BeforeSuperThis() {
super(1);
}
BeforeSuperThis(int x) {
consume(this); // 把 this 传出去
super(x);
}
static void consume(BeforeSuperThis b) {}
}
BeforeSuperThis.java:11: error: cannot reference this before supertype constructor has been called
consume(this);
^
1 error
限制的目的是保证「对象还没有完成构造」这个事实不会被代码忽略。父构造器可能调用可覆写方法,此时子类字段若已初始化,也必须是通过 prologue 合法写入的。
在 super() 前初始化字段03
JEP 513 的另一个目标是把「字段初始化」提前:父构造器可能调用被子类覆写的方法,如果子类字段还没赋值,覆写方法会看到默认值。prologue 允许先给无初始化器 的字段赋值,再调用 super。
class Parent {
Parent() {
System.out.println("Parent body");
hook();
}
void hook() {
System.out.println("Parent hook");
}
}
public class PrologueOrder extends Parent {
private final String state;
PrologueOrder() {
state = "ready";
System.out.println("Child prologue");
super();
System.out.println("Child epilogue, state=" + state);
}
@Override
void hook() {
System.out.println("Child hook, state=" + state);
}
public static void main(String[] args) {
new PrologueOrder();
}
}
Child prologue Parent body Child hook, state=ready Child epilogue, state=ready
输出验证了两件事:子类 prologue 先于父构造器体执行;父构造器调用覆写方法时,state 已经是 ready。final 字段只要没有初始化器,也可以在 prologue 中赋值一次。
- 只能给本类声明的字段赋值;不能给父类字段赋值。
- 字段不能有初始化器;
private int value = 7;再在 prologue 赋值会编译失败。 - prologue 里的赋值是 single assignment 规则的一部分;不要重复赋值 final 字段。
- 不要在 prologue 里启动线程或注册回调,避免 this 逃逸。
这条能力是为「父构造器调用可覆写方法」的安全性设计的,不是鼓励在构造器里做复杂业务。复杂初始化仍然应该放进工厂方法或依赖注入。
this(...) 之前也可以校验04
this(...) 与 super(...) 一样可以出现在 prologue 之后。常见用法是重载构造器先做统一校验,再委托给主构造器,避免每个重载都重复检查。
class Base5 {
Base5(int x) {
System.out.println("Base5(" + x + ")");
}
}
public class ThisDelegation extends Base5 {
private final int value;
ThisDelegation(int x) {
if (x < 0) {
throw new IllegalArgumentException("negative");
}
this(x, 1);
}
ThisDelegation(int x, int factor) {
super(x * factor);
this.value = x * factor;
}
public static void main(String[] args) {
System.out.println("value=" + new ThisDelegation(5).value);
try {
new ThisDelegation(-1);
} catch (IllegalArgumentException e) {
System.out.println("rejected=" + e.getMessage());
}
}
}
Base5(5) value=5 rejected=negative
- 校验放在委托构造器的 prologue,主构造器只负责赋值。
this(...)和super(...)不能同时出现在一个构造器体里。- prologue 不能读取主构造器将要设置的字段;它只能使用参数和局部变量。
- 如果校验依赖父类状态,必须等 super 返回后在 epilogue 做。
重载链越深,prologue 越容易重复。把纯参数校验抽成静态方法仍然可以;灵活构造器体的价值是让失败发生在 super 之前且代码在构造器里可见。
常见坑与边界05
- 给有初始化器 的字段赋值:编译错误,不是运行期问题。
- 读字段 / 调实例方法:即使方法不访问字段,也不允许;编译器按语法上下文拒绝。
- 把 this 传给 lambda 或方法:同样属于引用当前实例,编译失败。
- 在 prologue 里 return:不允许;epilogue 里可以
return;,但不能return value;。 - 以为父类字段已初始化:prologue 阶段父构造器还没运行,父类字段仍是默认值。
- record 的紧凑构造器:规则不同,不要直接把普通类的 prologue 经验套到 record。
class Base4 {
Base4() {}
}
public class BeforeSuperInitialized extends Base4 {
private int value = 7;
BeforeSuperInitialized() {
value = 9; // 字段有初始化器
super();
}
}
BeforeSuperInitialized.java:9: error: cannot assign initialized field 'value' before supertype constructor has been called
value = 9;
^
1 error
| 旧写法 | JDK 25 写法 | 注意 |
|---|---|---|
super(validate(x)) | prologue 里 if + throw | 失败发生在 super 之前 |
| 静态工厂做参数校验 | 构造器 prologue | 异常类型更直接 |
| 字段只能在 super 后赋值 | 无初始化器 字段可提前赋值 | 有初始化器 的字段不行 |
| 重载各自校验 | this(...) 前统一校验 | 避免重复逻辑 |
JEP 513 在 22–24 预览,25 正式。预览期编译的 class 仍带 65535 minor,不能跨版本运行;升级后要重新编译。
版本边界BOUNDARY
这张表比 API 本身更值得记。写代码时先确认你的运行时版本。
| 特性 / API | 状态 | 说明 |
|---|---|---|
| 灵活构造器体 | 22–24 预览 / 25 正式 | JEP 513;25 起无需 --enable-preview。 |
| super()/this() 前的语句 | 25 允许 | prologue 不能引用正在构造的实例。 |
| 给无初始化器 字段赋值 | 25 允许 | 含 final 字段的一次赋值。 |
| 给有初始化器 字段赋值 | 25 禁止 | cannot assign initialized field。 |
| prologue 中 return | 25 禁止 | epilogue 可以 return;。 |
| record 紧凑构造器 | 规则不同 | 不要把普通类 prologue 直接套用。 |
费曼自检SELF CHECK
先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。
01. super() 之前可以写哪些语句?
可以使用参数、局部变量、静态成员,做条件判断、抛异常、try/catch,并给本类没有初始化器 的字段赋值;不能读取本类字段、调用实例方法、使用 this,也不能在 prologue 里 return。
02. 为什么允许给字段赋值,却不允许读字段?
赋值是为了在父构造器调用可覆写方法前建立子类的不变量;读取则意味着依赖一个尚未完成构造的对象状态,容易看到默认值或半初始化状态,所以被禁止。
03. prologue 与 epilogue 的执行顺序是什么?
子类 prologue 先执行,然后调用父构造器(父类 prologue/构造器体/子类 epilogue 依次展开),super 返回后执行子类 epilogue。整体是 prologue 自下而上、epilogue 自上而下。
04. 给有初始化器 的字段在 super() 前赋值会怎样?
编译失败:cannot assign initialized field ... before supertype constructor has been called。初始化器 已经负责该字段,prologue 不能再写。
学习进度
记录保存在本机浏览器 · 不上传