构造器与初始化顺序
学习目标
- 知道构造器与普通方法的区别、重载规则和默认构造器条件。
- 使用
this(...)、super(...)复用当前类或父类的初始化逻辑。 - 能按静态初始化、实例字段、初始化块、父类构造器和子类构造器判断执行顺序。
- 避免在构造阶段调用可被子类重写的方法,维护对象初始化不变式。
核心知识点
专业术语
- 构造器(constructor):创建对象时执行的初始化代码,没有返回类型,不能被继承或重写。
- 构造器重载(constructor overloading):同一个类中用不同参数列表提供多种创建入口。
- 初始化块(initializer block):每次实例创建前执行的共享初始化片段;静态初始化块属于类初始化。
- 默认构造器(default constructor):类完全没有声明构造器时由编译器提供的无参构造器。
- 初始化不变式(initialization invariant):构造完成后对象必须满足的状态约束。
白话解释与边界
构造器像“对象出厂检查”:对象从 new 开始,只有初始化完成才能交给调用者。一个类只要声明了任意构造器,编译器就不会再自动补无参构造器;需要无参入口必须自己声明。构造器第一条语句可以是 this(...) 或 super(...),不能同时调用二者。
创建子类对象时,先完成父类静态初始化,再完成子类静态初始化;随后执行父类实例字段/初始化块、父类构造器,再执行子类实例字段/初始化块和子类构造器。初始化阶段不要调用可被子类重写的方法,因为子类字段可能还没有准备好。
常用用法
无参构造器:创建默认状态对象
显式无参构造器可以建立安全默认值;不要把“没有写构造器”和“总有无参构造器”混为一谈。
// 语义:显式无参构造器可以建立安全默认值。
class User {
private String name;
User() {
name = "guest";
}
String name() {
return name;
}
}
System.out.println(new User().name());
// 输出:guest带参构造器:创建满足不变式的对象
在构造器入口校验边界,避免创建后还要调用“修复方法”;异常应带出哪个参数不合法。
// 语义:在构造器入口校验边界,避免创建后还要调用“修复方法”。
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构造器重载:提供不同创建入口
重载只由参数列表区分;让一个构造器委托给“主构造器”能避免校验和默认值分叉。
// 语义:重载只由参数列表区分。
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:1this(...):委托当前类构造器
this(...) 必须是构造器第一条语句,且不能在同一构造器里再次调用另一个构造器形成循环。
// 语义: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,0super(...):调用父类构造器
如果没有显式 super(...),编译器会尝试插入无参父类构造调用;父类没有无参构造器时必须显式传参。
// 语义:如果没有显式 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实例字段初始化:先于构造器主体执行
实例字段初始化表达式和实例初始化块按源码顺序执行,然后才进入构造器主体;不要依赖与声明顺序不同的隐藏副作用。
// 语义:实例字段初始化表达式和实例初始化块按源码顺序执行,然后才进入构造器主体。
// 初始状态:source 初始为 "default"。
class Config {
private final String source = "default";
// 初始状态:source 的初始值为 "default"。
Config() {
System.out.println(source);
// 输出:default
}
}
new Config();static 初始化块:类首次初始化时执行
只有触发类初始化的主动使用才会执行静态初始化块;静态初始化失败会让类初始化失败,不要在其中做难以恢复的外部副作用。
// 语义:只有触发类初始化的主动使用才会执行静态初始化块。
// 初始状态:name 初始为 "registry"。
class Registry {
static String name = "registry";
// 初始状态:name 当前为 "registry"。
static {
System.out.println("load");
// 输出:load
}
}
System.out.println(Registry.name);
// 输出:registry初始化块:多个构造器共享实例准备
初始化块会复制到每个构造器执行路径;逻辑较复杂时优先使用私有方法或主构造器,保持初始化顺序可读。
// 语义:初始化块会复制到每个构造器执行路径。
class Token {
private final String value;
{
value = "ready";
}
Token() {}
String value() {
return value;
}
}
System.out.println(new Token().value());
// 输出:ready不常用但需要知道
父子类初始化顺序:用日志验证
静态部分每个类通常只执行一次,实例部分每次 new 都执行;初始化顺序是排查构造阶段空值和日志顺序的关键。
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 初始化或构造后显式方法,避免依赖未准备状态。
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();私有构造器:限制实例化入口
私有构造器可用于工具类、工厂或受控实例创建;不要仅为“阻止误用”就把正常领域对象做成静态工具。
class Ids {
private Ids() {}
static String prefix() {
return "U-";
}
}
System.out.println(Ids.prefix());
// 输出:U-继续阅读
- 类与对象:
this、字段、实例方法和对象生命周期。 - 封装、继承与多态:父子类型的构造和动态分派边界。
static、final与代码组织:静态初始化、常量和不可变字段。
简单案例
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(...)都必须是构造器第一条语句,且二者不能同时出现。- 父类构造器先于子类实例字段初始化和构造器主体,不能在父类构造阶段读取子类完整状态。
- 静态初始化通常每个类只执行一次,实例初始化每次创建对象都执行;不要把二者混为一谈。
- 构造器不应调用可被重写的方法,否则动态分派可能观察到子类未初始化字段。
课后小问
一个类声明了
User(String name)后,为什么new User()可能编译失败? 答案:只要类声明了任意构造器,编译器就不会再自动生成无参构造器。 解析:如果业务需要两种创建方式,应显式写无参构造器并定义默认值,而不是依赖编译器的隐式行为。创建
new Child()时,为什么父类构造器会先于子类字段和构造器? 答案:子类依赖父类部分先建立基础状态,JVM 按父类实例初始化、父类构造、子类实例初始化、子类构造的顺序执行。 解析:这条顺序保证父类不变式先成立,也解释了为什么父类构造器调用可重写方法会看到子类默认值。
本节小结
- 构造器建立对象不变式,重载入口应委托给一个清晰的主构造器。
this(...)复用当前类构造器,super(...)负责显式传递父类初始化参数。- 静态初始化、实例字段、初始化块和构造器都有明确先后顺序。
- 构造阶段避免开放调用,防止子类未完成初始化就参与动态分派。
快速回顾
- 能判断默认构造器何时存在。
- 能写出使用
this(...)和super(...)的构造器链。 - 能按父子类和静态/实例阶段说明初始化顺序。
- 能发现构造器调用可重写方法带来的空值风险。
阿源的知识库