[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

用代码验证

对类 <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 种情形之一 ⬇️

  1. 显式调用 <math xmlns="http://www.w3.org/1998/Math/MathML"> C C </math>C 的 superclass 的构造函数
  2. 显式调用 <math xmlns="http://www.w3.org/1998/Math/MathML"> C C </math>C 中的另一个构造函数
  3. 如果上述的情形 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 两个类之间有继承关系,对应的类图如下 ⬇️

classDiagram A1 <|-- B1 A1 : A1() B1 : int temp B1 : 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 的类图如下 ⬇️

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

参考资料

相关推荐
cynicme5 小时前
力扣3318——计算子数组的 x-sum I(偷懒版)
java·算法·leetcode
天若有情6736 小时前
【java EE】IDEA 中创建或迁移 Spring 或 Java EE 项目的核心步骤和注意事项
后端·spring·java-ee·intellij-idea
青云交6 小时前
Java 大视界 -- Java 大数据在智能教育学习效果评估与教学质量改进实战
java·实时分析·生成式 ai·个性化教学·智能教育·学习效果评估·教学质量改进
崎岖Qiu6 小时前
【设计模式笔记17】:单例模式1-模式分析
java·笔记·单例模式·设计模式
Lei活在当下6 小时前
【现代 Android APP 架构】09. 聊一聊依赖注入在 Android 开发中的应用
java·架构·android jetpack
不穿格子的程序员7 小时前
从零开始刷算法-栈-括号匹配
java·开发语言·
lkbhua莱克瓦247 小时前
Java练习-正则表达式 1
java·笔记·正则表达式·github
yue0087 小时前
C#类继承
java·开发语言·c#
大鱼七成饱7 小时前
💥 从崩溃到稳定:我踩过的 Rust Tokio 线程池坑(含代码示例)
后端
喵个咪8 小时前
开箱即用的GO后台管理系统 Kratos Admin - 站内信
后端·微服务·go