[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

参考资料

相关推荐
r0ad3 小时前
如何用RAG增强的动态能力与大模型结合打造企业AI产品?
后端·llm
杨杨杨大侠3 小时前
手把手教你写 httpclient 框架(三)- 动态代理与请求处理机制
java·okhttp·github
excel4 小时前
PM2 Cluster 模式下 RabbitMQ 队列并行消费方案
前端·后端
IT_陈寒4 小时前
React性能优化:这5个被90%开发者忽略的Hooks用法,让你的应用快3倍
前端·人工智能·后端
程序员爱钓鱼5 小时前
Go语言实战案例-项目实战篇:使用Go调用第三方API(如天气、翻译)
后端·google·go
追逐时光者13 小时前
一套开源、美观、高性能的跨平台 .NET MAUI 控件库,助力轻松构建美观且功能丰富的应用程序!
后端·.net
码事漫谈14 小时前
重构的艺术:从‘屎山’恐惧到优雅掌控的理性之旅
后端
码事漫谈14 小时前
C++框架中基类修改导致兼容性问题的深度分析与总结
后端
华仔啊20 小时前
王者段位排行榜如何实现?Redis有序集合实战
java·redis·后端