杰瑞科技汇

Java构造函数调用顺序是怎样的?

核心原则

在 Java 中,当一个对象被创建时(使用 new 关键字),其构造函数的调用遵循一个固定的顺序,这个顺序是由 Java 语言规范严格定义的,这个顺序可以总结为以下几步:

Java构造函数调用顺序是怎样的?-图1
(图片来源网络,侵删)
  1. 父类静态初始化块
  2. 子类静态初始化块
  3. 父类实例初始化块 / 父类成员变量初始化
  4. 父类构造函数
  5. 子类实例初始化块 / 子类成员变量初始化
  6. 子类构造函数

为了更好地理解,我们用一个流程图来表示:

graph TD
    A[创建子类对象 new SubClass()] --> B[加载 SubClass 类和父类 SuperClass 类];
    B --> C[执行 SuperClass 静态初始化块];
    C --> D[执行 SubClass 静态初始化块];
    D --> E[为 SuperClass 分配内存空间];
    E --> F[执行 SuperClass 实例初始化块/成员变量初始化];
    F --> G[调用 SuperClass 的构造函数];
    G --> H[为 SubClass 分配内存空间];
    H --> I[执行 SubClass 实例初始化块/成员变量初始化];
    I --> J[调用 SubClass 的构造函数];
    J --> K[对象创建完成];

详细步骤与代码示例

下面我们通过一个具体的代码示例来一步步追踪这个过程。

类的定义

我们定义一个父类 SuperClass 和一个子类 SubClass

// 父类 SuperClass
class SuperClass {
    // 父类静态成员变量
    public static String staticParentField = "父类静态成员变量 - 初始化";
    // 父类实例成员变量
    public String instanceParentField = "父类实例成员变量 - 初始化";
    // 父类静态初始化块
    static {
        System.out.println("(1) 父类静态初始化块执行");
    }
    // 父类实例初始化块
    {
        System.out.println("(4) 父类实例初始化块执行");
    }
    // 父类无参构造函数
    public SuperClass() {
        // 构造函数中的第一行代码,如果没有显式调用,默认是 super()
        System.out.println("(5) 父类构造函数执行");
    }
}
// 子类 SubClass
class SubClass extends SuperClass {
    // 子类静态成员变量
    public static String staticChildField = "子类静态成员变量 - 初始化";
    // 子类实例成员变量
    public String instanceChildField = "子类实例成员变量 - 初始化";
    // 子类静态初始化块
    static {
        System.out.println("(2) 子类静态初始化块执行");
    }
    // 子类实例初始化块
    {
        System.out.println("(6) 子类实例初始化块执行");
    }
    // 子类构造函数
    public SubClass() {
        // super() 在这里被隐式调用
        System.out.println("(7) 子类构造函数执行");
    }
}

创建对象并观察输出

我们在 Main 类中创建一个 SubClass 的实例。

Java构造函数调用顺序是怎样的?-图2
(图片来源网络,侵删)
public class Main {
    public static void main(String[] args) {
        System.out.println("--- 开始创建 SubClass 对象 ---");
        SubClass sub = new SubClass();
        System.out.println("--- SubClass 对象创建完成 ---");
    }
}

执行结果:

--- 开始创建 SubClass 对象 ---
(1) 父类静态初始化块执行
(2) 子类静态初始化块执行
(4) 父类实例初始化块执行
(5) 父类构造函数执行
(6) 子类实例初始化块执行
(7) 子类构造函数执行
--- SubClass 对象创建完成 ---

结果分析

让我们对照上面的核心原则来分析输出结果:

  1. 父类静态初始化块 (1)

    • new SubClass() 被执行时,JVM 首先需要加载 SubClass 类,由于 SubClass 继承自 SuperClassSuperClass 类也会被加载。
    • 类加载时,静态成员变量和静态初始化块会按照在代码中出现的顺序执行,父类的静态部分先被执行。
  2. 子类静态初始化块 (2)

    在父类静态部分执行完毕后,轮到子类的静态部分执行,同样,这是类加载过程的一部分。

  3. 父类实例初始化块 (4) 和 父类构造函数 (5)

    • 静态部分执行完毕后,真正的对象创建开始了。
    • new SubClass() 会先为父类 SuperClass 的部分分配内存空间。
    • 分配完内存后,会执行实例初始化块和成员变量的初始化,它们在构造函数之前执行。
    • 父类的构造函数 (5) 被调用,这是最关键的一步:子类构造函数在执行自己的代码之前,必须先调用父类的构造函数,如果没有显式使用 super(...),编译器会自动在子类构造函数的第一行插入一个 super() 调用。
  4. 子类实例初始化块 (6) 和 子类构造函数 (7)

    • 父类构造函数执行完毕后,对象创建流程返回到子类。
    • 为子类 SubClass 的部分分配内存空间。
    • 执行子类的实例初始化块和成员变量的初始化。
    • 执行子类自己的构造函数 (7) 的主体部分。

重要规则与注意事项

super() 的调用

  • 规则:子类构造函数的第一行代码必须是 super(...)(调用父类构造函数)或 this(...)(调用本类其他构造函数),如果两者都没有,编译器会自动在第一行插入一个无参的 super()
  • 原因:这确保了在创建子类对象时,其父类部分已经被正确地初始化,父类可能定义了子类所依赖的状态或方法,因此必须先初始化父类。

错误示例:

class SubClass extends SuperClass {
    public SubClass() {
        System.out.println("This is wrong!"); // 编译错误!
        // 因为 super() 必须是第一行,而这里没有
    }
}

this() 的调用

  • 规则this(...)super(...) 不能同时出现在同一个构造函数中,因为它们都必须是构造函数的第一行。
  • 目的this(...) 用于在一个构造函数中调用同一个类的另一个构造函数,以实现代码复用(避免重复的初始化代码)。

正确示例:

class Car {
    private String model;
    private int year;
    public Car() {
        this("Default Model", 2025); // 调用另一个构造函数
        System.out.println("Car with default model created.");
    }
    public Car(String model, int year) {
        this.model = model;
        this.year = year;
        System.out.println("Car created: " + model + ", " + year);
    }
}

静态部分 vs. 实例部分

  • 静态初始化块:只在类第一次被加载到 JVM 时执行一次,无论你创建多少个该类的对象,它都只执行一次。
  • 实例初始化块:在每次创建对象时都会执行,并且在构造函数之前执行,当初始化代码对于多个构造函数是通用的,但又不想写成一个单独的方法时,实例初始化块非常有用。

构造函数链

这个过程形成了一个“构造函数链”(Constructor Chaining),从最顶层的父类一直向下传递到最终的子类,这确保了对象被从上到下、完整地构建起来。


总结表格

调用顺序 组件 执行时机 执行次数
1 父类静态初始化块 类加载时 1 次
2 子类静态初始化块 类加载时 1 次
3 父类实例初始化块 / 成员变量初始化 创建对象时,父类构造函数前 每次创建对象
4 父类构造函数 创建对象时,子类构造函数前 每次创建对象
5 子类实例初始化块 / 成员变量初始化 创建对象时,子类构造函数前 每次创建对象
6 子类构造函数 创建对象时 每次创建对象

理解这个调用顺序对于理解 Java 对象的生命周期、继承机制以及调试复杂的初始化问题至关重要。

分享:
扫描分享到社交APP
上一篇
下一篇