[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

参考资料

相关推荐
法欧特斯卡雷特20 小时前
Kotlin 2.3.0 现已发布!又有什么好东西?
后端·架构·开源
要开心吖ZSH20 小时前
应用集成平台-系统之间的桥梁-思路分享
java·kafka·交互
TsengOnce20 小时前
阿里云ECS多版本JDK切换
java·python·阿里云
wearegogog12320 小时前
基于C#的FTP客户端实现方案
java·网络·c#
听风吟丶20 小时前
Java NIO 深度解析:从核心组件到高并发实战
java·开发语言·jvm
野生技术架构师20 小时前
Java面试题及答案总结(互联网大厂新版)
java·面试·状态模式
a努力。20 小时前
小红书Java面试被问:ThreadLocal 内存泄漏问题及解决方案
java·jvm·后端·算法·面试·架构
此生只爱蛋20 小时前
【Redis】String 字符串
java·数据库·redis
C++业余爱好者20 小时前
Java开发中Entity、VO、DTO、Form对象详解
java·开发语言
serendipity_hky20 小时前
【go语言 | 第4篇】goroutine模型和调度策略
后端·性能优化·golang