
本文深入剖析java对象初始化顺序,解释为何子类字段在父类构造器调用重写方法后又被默认值覆盖,导致输出null,并提供符合《effective java》规范的安全实践方案。
本文深入剖析java对象初始化顺序,解释为何子类字段在父类构造器调用重写方法后又被默认值覆盖,导致输出null,并提供符合《effective java》规范的安全实践方案。
在Java中,对象的初始化顺序严格遵循JVM规范,而非代码书写顺序。上述示例输出null的根本原因,正在于字段初始化(field initializer)发生在父类构造器执行完毕之后、子类构造器主体执行之前——而此时父类构造器已通过init(f)间接修改了子类字段data,但随后却被子类声明中的Integer data = null;覆盖。
初始化时序详解(以new Sub(10)为例)
-
调用
Sub(int f)构造器 → 隐式/显式调用super(f) -
执行
Super(int f)构造器:- 调用抽象方法
init(f)→ 实际执行Sub.init(10) -
Sub.init(10)将this.data赋值为10(自动装箱为Integer.valueOf(10))
- 调用抽象方法
-
返回至
Sub构造器后,执行字段初始化语句:Integer data = null; // ← 关键!等价于在super()后执行 data = null;
此时
data被重新赋值为null,覆盖了上一步设置的10。 - 子类构造器主体(空)执行完毕,对象初始化完成。
因此,sub.showData()打印null是确定性行为,而非偶然。
代码验证:观察实际执行流
class Sub extends Super {
Integer data = null;
Sub(int f) {
System.out.println("【Step 1】Sub constructor starts, about to call super(f)");
super(f);
System.out.println("【Step 3】After super() returns: data = " + data); // 输出 null
}
@Override
void init(int f) {
System.out.println("【Step 2】Inside Sub.init(" + f + "): setting data = " + f);
data = f; // 此时data为10
}
}运行输出清晰印证时序:
立即学习“Java免费学习笔记(深入)”;
【Step 1】Sub constructor starts, about to call super(f) 【Step 2】Inside Sub.init(10): setting data = 10 【Step 3】After super() returns: data = null
正确实践:避免构造器中调用可重写方法
正如《Effective Java》第19条明确指出:“构造器绝不可调用可被重写的方法”(Constructors must not invoke overridable methods)。这是面向对象设计的安全红线,因为此时子类对象尚未完成初始化,状态不一致。
✅ 推荐解决方案:
-
方案1:使用
final方法或私有方法(无法被重写)abstract class Super { Super(int f) { initImpl(f); // 私有方法,安全 } private void initImpl(int f) { /* ... */ } } -
方案2:延迟初始化(Lazy Initialization)
将逻辑移至首次访问时触发:class Sub extends Super { private Integer data; private boolean initialized = false; Sub(int f) { super(f); } @Override void init(int f) { if (!initialized) { data = f; initialized = true; } } } -
方案3:使用静态工厂方法替代构造器(推荐)
class Sub extends Super { private final Integer data; private Sub(int f) { super(0); // 避免在super中触发init this.data = f; } public static Sub create(int f) { return new Sub(f); } }
总结
Java初始化顺序的确定性(super() → 字段初始化 → 构造器体)是双刃剑:它保障了可预测性,但也要求开发者深刻理解其语义。当父类构造器调用子类重写方法时,子类字段可能处于“半初始化”状态,后续字段初始化语句会悄然覆盖已有值。最根本的解决之道,是恪守构造器不调用可重写方法的原则——这不仅是技术细节,更是健壮面向对象设计的基石。


















