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

背景

JDK 25 已经发布了,其中一个变化是对 JEP 513: Flexible Constructor Bodies 的支持。JDK 25 放松了对构造函数的限制,它支持如下的写法 ⬇️ (代码来自 JEP 513: Flexible Constructor Bodies,有改动)

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 小节(在下图绿色框的位置)⬇️

image.png

我画了流程图来展示上图绿框里的 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: 执行实例化语句块以及实例字段的赋值语句n(按照它们在 java 代码中的顺序执行)"]
six --> seven["Step 7: 执行当前构造函数中的剩余语句"]
seven --> End["结束"]
four --> |"用了 this(...),n那就调用对应的构造函数,n然后前往 Step 7"| seven
four --> |"用了 super(...),n那就调用对应的构造函数,n然后前往 Step 6"| six

用代码验证

对类 CC 任意一个构造函数 constructorconstructor 而言,constructorconstructor 中会出现以下 3 种情形之一 ⬇️

  1. 显式调用 CCsuperclass 的构造函数
  2. 显式调用 CC 中的另一个构造函数
  3. 如果上述的情形 1 和情形 2 都不成立,那么 constructorconstructor 中会隐式调用 CCsuperclass 的默认构造函数(除非 CC 刚好是 java.lang.Object)

我们分别来看每种情形的具体情况。

情形 1: 显式调用 superclass 的构造函数

请将以下代码保存为 Case1.java(其实也可以用其他文件名,但是为了描述方便,就把它命名为 Case1.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) 方法,这个方法会在展示入参 nn 之后,返回 nn

用以下命令可以编译 Case1.java ⬇️

javac Case1.java

注意,由于 Case1.java 里用到了 JDK 25 的新特性,所以 javac 的版本需要能支持 JDK 25 才能编译通过。

使用 javac -version 命令可以查看 javac 的版本。在我电脑上,该命令的运行结果如下

javac 25

我们需要确定下表中 甲乙丙丁 4 部分代码的执行顺序。

哪一部分描述用大白话来描述
the prologue (of the constructor body)B1()这个构造函数中 super() 之前的代码
an invocation of a superclass constructorB1()这个构造函数中 super() 那一行
the instance initializers and instance variable initializers for this class实例化语句块(也就是 {} 里的语句)以及对实例字段的赋值语句
the epilogue (of this constructor)B1()这个构造函数中 super() 之后的代码
image.png

执行 java B1 命令可以运行 B1 类中的 main 方法。虽然这个 main 方法的方法体是空的,但由于它是一个实例方法,所以在调用它之前,会先创建 B1 类的一个实例,这样 B1 类中的构造函数就会被调用。 运行结果如下

1
2
3
4
5

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

情形 2: 显式调用当前类的另一个构造函数

请将以下代码保存为 Case2.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 classB2()这个构造函数中 this(...) 那一行
the epilogue (of this constructor)B2()这个构造函数中 this(...) 之后的代码
image.png

用以下命令可以编译 Case2.java 以及运行 B2 中的 main 方法。

javac Case2.java
java B2

运行结果如下 ⬇️

1
2
3
4
5
6
7

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

情形 3: 隐式调用 superclass 的默认构造函数

请将以下代码保存为 Case3.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() 这个构造函数中的代码
image.png

用以下命令可以编译 Case3.java 以及运行 B3 中的 main 方法。

javac Case3.java
java B3

运行结果如下 ⬇️

1
2
3
4
5

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

一个复杂的例子

请将以下代码保存为 Complex.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 方法。

javac Complex.java
java B

运行结果如下 ⬇️

1
2
3
4
5
6
7
8
9
10
11
12
13

参考资料

  • The Java® Language Specification 中的
    • 12.5. Creation of New Class Instances 小节
  • JEP 513: Flexible Constructor Bodies
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]