[Java] JDK 25 新变化之构造函数的执行逻辑

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 个步骤(在流程图中忽略了所有抛异常的情况)⬇️

graph TD Start["开始"] --> one["Step 1: 参数处理"] one --> two{"Step 2:\n当前构造函数中是否有\nthis(...)/super(...)?"} two --> |是| three["Step 3: 执行 this(...)/super(...) 之前的语句"] three --> four{"Step 4:\n用了 this(...) 还是 super(...)?"} two --> |否| five["Step 5: 如果当前构造函数并非来自 java.lang.Object,\n那么当前构造函数中包含对 superclass 的默认构造函数的隐式调用"] five --> |"调用 superclass 的默认构造函数后,\n前往 Step 6"| six["Step 6: 执行实例化语句块以及实例字段的赋值语句"] six --> seven["Step 7: 执行当前构造函数中的剩余语句"] seven --> End["结束"] four --> |"用了 this(...),\n那就调用对应的构造函数,\n然后前往 Step 7"| seven four --> |"用了 super(...),\n那就调用对应的构造函数,\n然后前往 Step 6"| six

用代码验证

对类 C C C 任意一个构造函数 c o n s t r u c t o r constructor constructor 而言, c o n s t r u c t o r constructor constructor 中会出现以下 3 种情形之一 ⬇️

  1. 显式调用 C C C 的 superclass 的构造函数
  2. 显式调用 C C C 中的另一个构造函数
  3. 如果上述的情形 1 和情形 2 都不成立,那么 c o n s t r u c t o r constructor constructor 中会隐式调用 C C C 的 superclass 的默认构造函数(除非 C C 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 两个类之间有继承关系,对应的类图如下 ⬇️

classDiagram A1 <|-- B1 A1 : A1() B1 : int temp B1 : B1()

为了便于观察各个部分的执行顺序,我在 Util 类中定义了 displayAndGet(int) 方法,这个方法会在展示入参 n n n 之后,返回 n n 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 的类图如下 ⬇️

classDiagram A2 <|-- B2 A2 : A2() B2 : int temp B2 : B2() B2 : B2(String)

我们需要确定下表中 甲乙丙 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 的类图如下 ⬇️

classDiagram A3 <|-- B3 A3 : A3() B3 : int temp B3 : 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

参考资料

相关推荐
腻害兔6 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
GetcharZp7 小时前
抛弃低效SSH!这套Ansible+Web可视化神器,让你天天准点下班!
后端
码智社7 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
萧瑟余晖8 小时前
JDK 26 新特性详解
java·开发语言
马优晨9 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
weixin_4462608510 小时前
HACO:面向动态部署环境的对冲式智能计算可靠多智能体调度框架
后端·python·flask
ttwuai11 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
用户83562907805111 小时前
Python 实现 Excel 页面布局与打印设置自动化
后端·python
维天说11 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json