JDK 25
新变化之构造函数的执行逻辑
背景
JDK 25
已经发布了,其中一个变化是对 JEP 513: Flexible Constructor Bodies 的支持。JDK 25
放松了对构造函数的限制,它支持如下的写法 ⬇️ (代码来自 JEP 513: Flexible Constructor Bodies,有改动)
java
class Person {
int age;
Person(int age) {
if (age < 0)
throw new IllegalArgumentException("Age can't be negative number!");
this.age = age;
}
}
class Employee extends Person {
String officeID;
Employee(int age, String officeID) {
if (age < 18 || age > 67) {
// Now fails fast!
throw new IllegalArgumentException("Age is outside of expected range!");
}
this.officeID = officeID; // Initialize before calling superclass constructor!
super(age);
}
}
注意,在 Employee
类的构造函数中,super(age);
这一行之前还有其他语句,这样的代码在 JDK 25
之前会编译失败 。有了这样的调整后,构造函数实际的执行逻辑会变成什么样子呢?本文会对构造函数的执行逻辑进行探讨。请注意,本文的讨论忽略了所有抛异常的情况。
要点
严谨的描述请参考 The Java Language Specification 中的 12.5. Creation of New Class Instances 小节(在下图绿色框的位置)⬇️

我画了流程图来展示上图绿框里的 7
个步骤(在流程图中忽略了所有抛异常的情况)⬇️
用代码验证
对类 <math xmlns="http://www.w3.org/1998/Math/MathML"> C C </math>C 任意一个构造函数 <math xmlns="http://www.w3.org/1998/Math/MathML"> c o n s t r u c t o r constructor </math>constructor 而言, <math xmlns="http://www.w3.org/1998/Math/MathML"> c o n s t r u c t o r constructor </math>constructor 中会出现以下 3
种情形之一 ⬇️
- 显式调用 <math xmlns="http://www.w3.org/1998/Math/MathML"> C C </math>C 的
superclass
的构造函数 - 显式调用 <math xmlns="http://www.w3.org/1998/Math/MathML"> C C </math>C 中的另一个构造函数
- 如果上述的情形
1
和情形2
都不成立,那么 <math xmlns="http://www.w3.org/1998/Math/MathML"> c o n s t r u c t o r constructor </math>constructor 中会隐式调用 <math xmlns="http://www.w3.org/1998/Math/MathML"> C C </math>C 的superclass
的默认构造函数(除非 <math xmlns="http://www.w3.org/1998/Math/MathML"> C C </math>C 刚好是java.lang.Object
)
我们分别来看每种情形的具体情况。
情形 1
: 显式调用 superclass
的构造函数
请将以下代码保存为 Case1.java
(其实也可以用其他文件名,但是为了描述方便,就把它命名为 Case1.java
了)
java
class A1 {
A1() {
Util.displayAndGet(2);
}
}
class Util {
// 这个方法在输出入参 n 之后,返回 n。看似什么也没做。
// 提供这个方法是为了方便观察各个步骤的执行顺序。
static int displayAndGet(int n) {
IO.println(n);
return n;
}
}
class B1 extends A1 {
B1() {
int temp = Util.displayAndGet(1);
super();
temp = Util.displayAndGet(5);
}
{
int temp = Util.displayAndGet(3);
}
int temp = Util.displayAndGet(4);
void main() {
}
}
Case1.java
文件里的 A1
/B1
两个类之间有继承关系,对应的类图如下 ⬇️
为了便于观察各个部分的执行顺序,我在 Util
类中定义了 displayAndGet(int)
方法,这个方法会在展示入参 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 之后,返回 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n。
用以下命令可以编译 Case1.java
⬇️
bash
javac Case1.java
注意,由于 Case1.java
里用到了 JDK 25
的新特性,所以 javac
的版本需要能支持 JDK 25
才能编译通过。
使用 javac -version
命令可以查看 javac
的版本。在我电脑上,该命令的运行结果如下
text
javac 25
我们需要确定下表中 甲乙丙丁 4
部分代码的执行顺序。
哪一部分 | 描述 | 用大白话来描述 |
---|---|---|
甲 | the prologue (of the constructor body) | B1() 这个构造函数中 super() 之前的代码 |
乙 | an invocation of a superclass constructor | B1() 这个构造函数中 super() 那一行 |
丙 | the instance initializers and instance variable initializers for this class | 实例化语句块(也就是 {} 里的语句)以及对实例字段的赋值语句 |
丁 | the epilogue (of this constructor) | B1() 这个构造函数中 super() 之后的代码 |

执行 java B1
命令可以运行 B1
类中的 main
方法。虽然这个 main
方法的方法体是空的,但由于它是一个实例方法,所以在调用它之前,会先创建 B1
类的一个实例,这样 B1
类中的构造函数就会被调用。 运行结果如下
text
1
2
3
4
5
由此可见,实际的执行顺序是
- 甲
- 乙
- 丙
- 丁
情形 2
: 显式调用当前类的另一个构造函数
请将以下代码保存为 Case2.java
⬇️
java
class A2 {
A2() {
Util.displayAndGet(3);
}
}
class Util {
// 这个方法在输出入参 n 之后,返回 n。看似什么也没做。
// 提供这个方法是为了方便观察各个步骤的执行顺序。
static int displayAndGet(int n) {
IO.println(n);
return n;
}
}
class B2 extends A2 {
B2() {
int temp = Util.displayAndGet(1);
this("placeholder");
temp = Util.displayAndGet(7);
}
B2(String s) {
int temp = Util.displayAndGet(2);
super();
temp = Util.displayAndGet(6);
}
{
int temp = Util.displayAndGet(4);
}
int temp = Util.displayAndGet(5);
void main() {
}
}
B2
类中有 2
个构造函数,A2
/B2
的类图如下 ⬇️
我们需要确定下表中 甲乙丙 3
部分代码的执行顺序。
哪一部分 | 描述 | 用大白话来描述 |
---|---|---|
甲 | the prologue (of the constructor body) | B2() 这个构造函数中 this(...) 之前的代码 |
乙 | an invocation of another constructor in the same class | B2() 这个构造函数中 this(...) 那一行 |
丙 | the epilogue (of this constructor) | B2() 这个构造函数中 this(...) 之后的代码 |

用以下命令可以编译 Case2.java
以及运行 B2
中的 main
方法。
bash
javac Case2.java
java B2
运行结果如下 ⬇️
text
1
2
3
4
5
6
7
由此可见,实际的执行顺序是
- 甲
- 乙
- 丙
情形 3
: 隐式调用 superclass
的默认构造函数
请将以下代码保存为 Case3.java
⬇️
java
class A3 {
A3() {
Util.displayAndGet(1);
}
}
class Util {
// 这个方法在输出入参 n 之后,返回 n。看似什么也没做。
// 提供这个方法是为了方便观察各个步骤的执行顺序。
static int displayAndGet(int n) {
IO.println(n);
return n;
}
}
class B3 extends A3 {
B3() {
int temp = Util.displayAndGet(4);
temp = Util.displayAndGet(5);
}
{
int temp = Util.displayAndGet(2);
}
int temp = Util.displayAndGet(3);
void main() {
}
}
从 class
文件来看,情形 3
和情形 1
其实是一样的。java
代码里所谓的隐式调用在 class
文件中其实就是一个正常的函数调用。A3
/B3
的类图如下 ⬇️
我们的目标仍旧是确定下图中 甲乙丙 所对应的代码执行的顺序。
哪一部分 | 描述 | 用大白话来描述 |
---|---|---|
甲 | an implicit invocation of a superclass constructor with no arguments | 隐式调用 superclass 的默认构造函数 |
乙 | the instance initializers and instance variable initializers for this class | 实例化语句块(也就是 {} 里的语句)以及对实例字段的赋值语句 |
丙 | the epilogue (of this constructor) | B3() 这个构造函数中的代码 |

用以下命令可以编译 Case3.java
以及运行 B3
中的 main
方法。
bash
javac Case3.java
java B3
运行结果如下 ⬇️
text
1
2
3
4
5
由此可见,实际的执行顺序是
- 甲
- 乙
- 丙
一个复杂的例子
请将以下代码保存为 Complex.java
java
// 这个文件用到了 JDK 25 的特性,请用对应版本的 javac 进行编译
class A {
int f1 = Util.displayAndGet(4);
A() {
int temp = Util.displayAndGet(3);
super(); // 这一行显式调用 java.lang.Object 的构造函数
temp = Util.displayAndGet(6);
}
{
int temp = Util.displayAndGet(5);
}
}
class Util {
// 这个方法在输出入参 n 之后,返回 n。看似什么也没做。
// 提供这个方法是为了方便观察各个步骤的执行顺序。
static int displayAndGet(int n) {
IO.println(n);
return n;
}
}
class B extends A {
{
int temp = Util.displayAndGet(7);
}
int temp = Util.displayAndGet(8);
B() {
int temp = Util.displayAndGet(1);
this("placeholder"); // 这一行显式调用 B 中的另一个构造函数
temp = Util.displayAndGet(13);
}
B(String s) {
int temp = Util.displayAndGet(2);
super(); // 这一行显式调用 A 的构造函数
temp = Util.displayAndGet(12);
}
{
int temp = Util.displayAndGet(9);
}
int temp2 = Util.displayAndGet(10);
{
int temp = Util.displayAndGet(11);
}
void main() {
}
}
用以下命令可以编译 Complex.java
以及运行 B
中的 main
方法。
bash
javac Complex.java
java B
运行结果如下 ⬇️
text
1
2
3
4
5
6
7
8
9
10
11
12
13