Skip to content

构造器与初始化顺序 ​

学习目标 ​

  • 知道构造器与普通方法的区别、重载规则和默认构造器条件。
  • 使用 this(...)、super(...) 复用当前类或父类的初始化逻辑。
  • 能按静态初始化、实例字段、初始化块、父类构造器和子类构造器判断执行顺序。
  • 避免在构造阶段调用可被子类重写的方法,维护对象初始化不变式。

核心知识点 ​

专业术语 ​

  • 构造器(constructor):创建对象时执行的初始化代码,没有返回类型,不能被继承或重写。
  • 构造器重载(constructor overloading):同一个类中用不同参数列表提供多种创建入口。
  • 初始化块(initializer block):每次实例创建前执行的共享初始化片段;静态初始化块属于类初始化。
  • 默认构造器(default constructor):类完全没有声明构造器时由编译器提供的无参构造器。
  • 初始化不变式(initialization invariant):构造完成后对象必须满足的状态约束。

白话解释与边界 ​

构造器像“对象出厂检查”:对象从 new 开始,只有初始化完成才能交给调用者。一个类只要声明了任意构造器,编译器就不会再自动补无参构造器;需要无参入口必须自己声明。构造器第一条语句可以是 this(...) 或 super(...),不能同时调用二者。

创建子类对象时,先完成父类静态初始化,再完成子类静态初始化;随后执行父类实例字段/初始化块、父类构造器,再执行子类实例字段/初始化块和子类构造器。初始化阶段不要调用可被子类重写的方法,因为子类字段可能还没有准备好。

常用用法 ​

无参构造器:创建默认状态对象 ​

显式无参构造器可以建立安全默认值;不要把“没有写构造器”和“总有无参构造器”混为一谈。

java
// 语义:显式无参构造器可以建立安全默认值。
class User {
    private String name;

    User() {
        name = "guest";
    }

    String name() {
        return name;
    }
}

System.out.println(new User().name());
// 输出:guest

带参构造器:创建满足不变式的对象 ​

在构造器入口校验边界,避免创建后还要调用“修复方法”;异常应带出哪个参数不合法。

java
// 语义:在构造器入口校验边界,避免创建后还要调用“修复方法”。
class Port {
    private final int value;

    Port(int value) {
        if (value < 1 || value > 65535) {
            throw new IllegalArgumentException("port");
// 异常:throw new IllegalArgumentException("port") 立即进入异常路径。
// 初始状态:表达式为 throw new IllegalArgumentException("port")。
        }
        this.value = value;
    }

    int value() {
        return value;
    }
}

System.out.println(new Port(8080).value());
// 输出:8080

构造器重载:提供不同创建入口 ​

重载只由参数列表区分;让一个构造器委托给“主构造器”能避免校验和默认值分叉。

java
// 语义:重载只由参数列表区分。
class User {
    private final String name;
    private final int level;

    User(String name) {
        this(name, 1);
    }

    User(String name, int level) {
        this.name = name;
        this.level = level;
    }

    String summary() {
        return name + ":" + level;
    }
}

System.out.println(new User("Ann").summary());
// 输出:Ann:1

this(...):委托当前类构造器 ​

this(...) 必须是构造器第一条语句,且不能在同一构造器里再次调用另一个构造器形成循环。

java
// 语义:this(...) 必须是构造器第一条语句,且不能在同一构造器里再次调用另一个构造器形成循环。
class Point {
    private final int x;
    private final int y;

    Point() {
        this(0, 0);
        // 作用:通过 this(...) 委托当前类构造器。
    }

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    String text() {
        return x + "," + y;
    }
}

System.out.println(new Point().text());
// 输出:0,0

super(...):调用父类构造器 ​

如果没有显式 super(...),编译器会尝试插入无参父类构造调用;父类没有无参构造器时必须显式传参。

java
// 语义:如果没有显式 super(...),编译器会尝试插入无参父类构造调用。
class Person {
    protected final String name;

    Person(String name) {
        this.name = name;
    }
}

class Employee extends Person {
    Employee(String name) {
        super(name);
        // 作用:通过 super(...) 调用父类构造器。
    }
}

System.out.println(new Employee("Ann").name);
// 输出:Ann

实例字段初始化:先于构造器主体执行 ​

实例字段初始化表达式和实例初始化块按源码顺序执行,然后才进入构造器主体;不要依赖与声明顺序不同的隐藏副作用。

java
// 语义:实例字段初始化表达式和实例初始化块按源码顺序执行,然后才进入构造器主体。
// 初始状态:source 初始为 "default"。
class Config {
    private final String source = "default";
// 初始状态:source 的初始值为 "default"。

    Config() {
        System.out.println(source);
        // 输出:default
    }
}

new Config();

static 初始化块:类首次初始化时执行 ​

只有触发类初始化的主动使用才会执行静态初始化块;静态初始化失败会让类初始化失败,不要在其中做难以恢复的外部副作用。

java
// 语义:只有触发类初始化的主动使用才会执行静态初始化块。
// 初始状态:name 初始为 "registry"。
class Registry {
    static String name = "registry";
    // 初始状态:name 当前为 "registry"。

    static {
        System.out.println("load");
        // 输出:load
    }
}

System.out.println(Registry.name);
// 输出:registry

初始化块:多个构造器共享实例准备 ​

初始化块会复制到每个构造器执行路径;逻辑较复杂时优先使用私有方法或主构造器,保持初始化顺序可读。

java
// 语义:初始化块会复制到每个构造器执行路径。
class Token {
    private final String value;

    {
        value = "ready";
    }

    Token() {}

    String value() {
        return value;
    }
}

System.out.println(new Token().value());
// 输出:ready

不常用但需要知道 ​

父子类初始化顺序:用日志验证 ​

静态部分每个类通常只执行一次,实例部分每次 new 都执行;初始化顺序是排查构造阶段空值和日志顺序的关键。

java
class Parent {
    static {
        System.out.println("parent-static");
        // 输出:parent-static
    }

    {
        System.out.println("parent-instance");
        // 输出:parent-instance
    }

    Parent() {
        System.out.println("parent-constructor");
        // 输出:parent-constructor
    }
}

class Child extends Parent {
    static {
        System.out.println("child-static");
        // 输出:child-static
    }

    {
        System.out.println("child-instance");
        // 输出:child-instance
    }

    Child() {
        System.out.println("child-constructor");
        // 输出:child-constructor
    }
}

new Child();
// 返回:new Child() 创建 Child 实例;首次初始化时依次输出 parent-static、child-static、parent-instance、parent-constructor、child-instance、child-constructor。

构造器中调用可重写方法:初始化风险 ​

父类构造器运行时可能动态调用子类覆盖方法,而子类字段还未初始化;使用 private/final 初始化或构造后显式方法,避免依赖未准备状态。

java
class Base {
    Base() {
        print();
    }

    void print() {
        System.out.println("base");
        // 输出:base
    }
}

class Child extends Base {
    private String value = "child";
// 初始状态:value 的初始值为 "child"。

    @Override
    void print() {
        System.out.println(value);
        // 输出:null
    }
}

new Child();

私有构造器:限制实例化入口 ​

私有构造器可用于工具类、工厂或受控实例创建;不要仅为“阻止误用”就把正常领域对象做成静态工具。

java
class Ids {
    private Ids() {}

    static String prefix() {
        return "U-";
    }
}

System.out.println(Ids.prefix());
// 输出:U-

继续阅读 ​

简单案例 ​

java
public class InitializationDemo {
    static class Parent {
        static String kind = "parent-static";
        String name = "parent-field";

        Parent() {
            System.out.println(kind + ", " + name);
            // 输出:parent-static, parent-field
        }
    }

    static class Child extends Parent {
        static String kind = "child-static";
        String name = "child-field";

        Child() {
            System.out.println(kind + ", " + name);
            // 输出:child-static, child-field
        }
    }

    public static void main(String[] args) {
        new Child();
    }
}

这个案例把父类构造器和子类构造器的输出分开观察;更复杂的初始化顺序要结合静态字段、实例字段和初始化块的源码位置一起判断。

易混点 ​

  • 声明了带参构造器后不会自动保留无参构造器,需要时必须显式声明。
  • this(...) 和 super(...) 都必须是构造器第一条语句,且二者不能同时出现。
  • 父类构造器先于子类实例字段初始化和构造器主体,不能在父类构造阶段读取子类完整状态。
  • 静态初始化通常每个类只执行一次,实例初始化每次创建对象都执行;不要把二者混为一谈。
  • 构造器不应调用可被重写的方法,否则动态分派可能观察到子类未初始化字段。

课后小问 ​

  1. 一个类声明了 User(String name) 后,为什么 new User() 可能编译失败? 答案:只要类声明了任意构造器,编译器就不会再自动生成无参构造器。 解析:如果业务需要两种创建方式,应显式写无参构造器并定义默认值,而不是依赖编译器的隐式行为。

  2. 创建 new Child() 时,为什么父类构造器会先于子类字段和构造器? 答案:子类依赖父类部分先建立基础状态,JVM 按父类实例初始化、父类构造、子类实例初始化、子类构造的顺序执行。 解析:这条顺序保证父类不变式先成立,也解释了为什么父类构造器调用可重写方法会看到子类默认值。

本节小结 ​

  • 构造器建立对象不变式,重载入口应委托给一个清晰的主构造器。
  • this(...) 复用当前类构造器,super(...) 负责显式传递父类初始化参数。
  • 静态初始化、实例字段、初始化块和构造器都有明确先后顺序。
  • 构造阶段避免开放调用,防止子类未完成初始化就参与动态分派。

快速回顾 ​

  • 能判断默认构造器何时存在。
  • 能写出使用 this(...) 和 super(...) 的构造器链。
  • 能按父子类和静态/实例阶段说明初始化顺序。
  • 能发现构造器调用可重写方法带来的空值风险。