[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

由此可见,实际的执行顺序是

  1. 甲
  2. 乙
  3. 丙
  4. 丁

情形 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

由此可见,实际的执行顺序是

  1. 甲
  2. 乙
  3. 丙

情形 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

由此可见,实际的执行顺序是

  1. 甲
  2. 乙
  3. 丙

一个复杂的例子

请将以下代码保存为 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

参考资料

相关推荐
10mAh6 小时前
【Java】ArrayList 遍历删除为什么报错?三种写法与漏删复现
java·开发语言·windows
JavaGuide6 小时前
DeepSeek Harness 重磅更新,Codex、Claude Code 子智能体进官方插件列表了,还有 Git 工作树和思考翻译!
前端·后端
thefool1122666 小时前
多线程 4
java
Rain的Java大神之路6 小时前
[特殊字符]8年Java老兵转型AI Agent:90%的人挂在同一个坑,根本不用学Python!(万字实战复盘,建议收藏)
java·数据库·人工智能·后端·python·spring·ai编程
高频因子挖掘机7 小时前
A股分钟K线数据占内存:先缩小请求范围,还是换 DataFrame 工具?
后端·github·api
高级程序源7 小时前
springboot大学生体测系统72170-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·python·django·课程设计
IT研究室7 小时前
最新计算机毕业设计选题推荐-基于spring boot的文学名著分享系统-网站-文档指导-Java-springboot
java·spring boot·课程设计
denlaku7 小时前
JDK 13 新特性详解
java·开发语言
秋饼7 小时前
Java 后端企业级 LLM 网关实战:Spring Cloud Gateway 构建统一大模型接入层
java·ai·技术分享·后端开发
打工仔折腾 AI7 小时前
极空间用 Docker 部署 Photopea:网页版 PS 的私有化实践
运维·后端·python·docker·容器·langchain·ai agent 实战