Java 构造函数有一条规则,从 1.0 开始就没变过:super() 或 this() 必须是构造方法体的第一条语句。不是建议,是强制。不遵守编译器直接报错。
这个限制存在了近 30 年,大家早就习惯了,顶多抽个 static 辅助方法绕过去,很少去想它到底有没有道理。实际上 JVM 那边早就支持在 super() 前写代码了——当年为了适配内部类的编译器生成代码,JVM 规范被放宽过,但 Java 语言规范一直没跟着改。直到 JEP 513 在 Java 25 里正式定稿,这个缺口才终于补上。
别扭在哪
先说最常见的一种情况。子类想先校验参数,再传给父类构造函数。
class Employee extends Person {
Employee(..., int age) {
super(..., age); // 必须第一行
if (age < 18 || age > 67)
throw new IllegalArgumentException(...);
}
}
先 super() 了才校验,如果 age 不合法,父类构造函数已经白跑了一遍。
以往的标准解法是抽一个 static 辅助方法,把校验逻辑内联到 super() 参数里:
class Employee extends Person {
private static int verifyAge(int value) {
if (value < 18 || value > 67)
throw new IllegalArgumentException(...);
return value;
}
Employee(..., int age) {
super(..., verifyAge(age));
}
}
一个简单的边界检查要多包一层方法,读代码时还需跳转查看。
更隐蔽的问题:子类字段还没初始化就被访问了
Java 构造函数是从父类往下跑的:父类构造函数执行的时候,子类的字段还是默认值。如果父类构造函数间接访问了子类字段 —— 比如调了一个被子类覆盖的方法 —— 那看到的就是 null 或者 0。
class Person {
int age;
void show() {
System.out.println("Age: " + this.age);
}
Person(..., int age) {
this.age = age;
show(); // 这里调用的是子类的 show
}
}
class Employee extends Person {
String officeID;
@Override
void show() {
System.out.println("Age: " + this.age);
System.out.println("Office: " + this.officeID);
}
Employee(..., int age, String officeID) {
super(..., age);
this.officeID = officeID;
}
}
new Employee(42, "CAM-FORA") 的实际输出是:
Age: 42
Office: null
officeID 为 null,因为 Person 构造函数调用 show() 时,Employee 构造函数尚未执行到 this.officeID = officeID。即使声明为 final,字段也会被观察到两次 —— 第一次是默认值,第二次才是真正的赋值。Effective Java 第 19 条对此的建议是 “构造函数不能调用可覆盖的方法”。
Java 25 的解法
JEP 513 做了什么?就是把那条 “super() 必须第一行” 的规则给删了。仅此而已。效果是你可以这样写:
class Employee extends Person {
String officeID;
Employee(..., int age, String officeID) {
// 先校验
if (age < 18 || age > 67)
throw new IllegalArgumentException(...);
// 先初始化子类字段
this.officeID = officeID;
// 再调用父类构造函数
super(..., age);
}
}
new Employee(42, "CAM-FORA") 现在打印:
Age: 42
Office: CAM-FORA
参数校验在 super() 之前完成,子类字段在父类构造函数执行前就已处于有效状态。
Prologue 和 Epilogue
构造函数体现在分成两个阶段:
super() 之前的代码叫 prologue(序言),之后的叫 epilogue(尾声)。执行顺序变成了:
D 的 prologue
→ C 的 prologue
→ B 的 prologue
→ A 的 prologue
→ Object 构造函数体
→ A 的 epilogue
→ B 的 epilogue
→ C 的 epilogue
→ D 的 epilogue
Prologue 从最底层的子类往父类方向执行(自底向上),把各层字段初始化好,父类构造函数执行时所有字段均已就绪。Epilogue 再按传统方式从父类到子类(自顶向下)执行,此时可安全访问实例的任意状态。
不是完全没有限制
Prologue 里虽然能写代码,但不能乱来。这块代码处于 early construction context(早期构造上下文),核心限制就一条:不能用 this。
具体来说:
- 不能读任何实例字段,也不能调任何实例方法 —— 不管显式
this.xxx还是隐式xxx - 可以给字段赋值,但只能是那些声明时没有初始化器的字段(比如
int i;可以,String s = "hello";不行) - 不能用
return - 不能用
super访问父类的字段或方法
对象尚未构造完成时,读取到的值不可靠,因此禁止读取而只允许写入刚赋值的字段。
嵌套类有一个例外:内部类在 prologue 里可以访问外部类的实例和成员,因为外部类实例在此前已创建完成。
JVM 其实早就准备好了一切
JVM 规范在很多年前就已被放宽 —— 当时是为了让编译器能生成内部类相关的一些特殊代码。构造函数体的验证规则从一开始的 “super() 必须是第一条指令” 改成了 “允许前面有任意指令,只要不访问未初始化的实例就行”。
但那次改 JVM 的时候,没人顺手把 Java 语言规范也改了。于是产生了这样一个局面:JVM 能跑灵活的构造函数体,javac 却不让写。编译器自己生成的那些特殊代码用到了这个灵活性,但用户写不出来。
直到 JDK 25,这个缺口才终于补上。
从 Preview 到定稿
JEP 513 没有引入新关键字或语法糖,只是撤掉了一条历史限制。该特性在 JDK 22、23、24 上连续 preview 了三版,期间几乎没有改动,最终在 JDK 25 定稿。
Project Valhalla 的 value class(JEP 401)将在此基础之上进一步演进 —— 当 value class 的构造函数没有显式 super() 调用时,隐式调用会被放置在构造函数体末尾而非开头,即整个构造函数体都是 prologue。这意味着 value class 的初始化模型与普通类有根本性不同。
本文内容来源于 JEP 513: Flexible Constructor Bodies。