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

321 阅读6分钟

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 的构造函数

下方的流程图中一共有 7Step,我用 浅绿色 填充了情形 1 里实际执行的 Step,用 灰色 填充了情形 1 里未执行的 Step ⬇️

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

classDef withGreen fill:lightgreen
classDef withGrey fill:grey

class one,two,three,four,six,seven withGreen;
class five withGrey;

我们写点代码来验证一下。请将以下代码保存为 Case1.java(其实也可以用其他文件名,但是为了描述方便,就把它命名为 Case1.java 了)

// 以下代码用到了 JDK 25 的特性,请用对应版本的 javac 进行编译

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 个框里的语句应该是按照 Step 3 -> Step 4 -> Step 6 -> Step 7 的顺序执行的 ⬇️

image.png

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

1
2
3
4
5

由此可见,实际的执行顺序与我们预期的一样。

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

下方的流程图中一共有 7Step,我用 浅绿色 填充了情形 2 里实际执行的 Step,用 灰色 填充了情形 2 里未执行的 Step ⬇️

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

classDef withGreen fill:lightgreen
classDef withGrey fill:grey

class one,two,three,four,seven withGreen;
class five,six withGrey;

我们写点代码来验证一下。请将以下代码保存为 Case2.java ⬇️

// 以下代码用到了 JDK 25 的特性,请用对应版本的 javac 进行编译

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 个框里的语句应该是 Step 3 -> Step 4 -> Step 7 的顺序执行的 ⬇️

image.png

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

javac Case2.java
java B2

运行结果如下 ⬇️

1
2
3
4
5
6
7

由此可见,实际的执行顺序与我们预期的一样。

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

下方的流程图中一共有 7Step,我用 浅绿色 填充了情形 3 里实际执行的 Step,用 灰色 填充了情形 3 里未执行的 Step ⬇️

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

classDef withGreen fill:lightgreen
classDef withGrey fill:grey

class one,two,five,six,seven withGreen;
class three,four withGrey;

我们写点代码来验证一下。请将以下代码保存为 Case3.java ⬇️

// 以下代码用到了 JDK 25 的特性,请用对应版本的 javac 进行编译

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() {

    }
}

A3/B3 的类图如下 ⬇️

classDiagram
A3 <|-- B3
A3 : A3()
B3 : int temp
B3 : B3()

按照前文的分析,下图中 3 个框里的语句应该是 Step 5 -> Step 6 -> Step 7 的顺序执行的 ⬇️

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

参考资料