
Java对象初始化时,父类构造器先于子类字段初始化执行;若父类构造器中调用了可被重写的抽象方法,而该方法在子类中修改了尚未初始化的字段,则后续子类字段的显式赋值(如Integer data = null;)会覆盖此前的修改,最终导致null输出。
java对象初始化时,父类构造器先于子类字段初始化执行;若父类构造器中调用了可被重写的抽象方法,而该方法在子类中修改了尚未初始化的字段,则后续子类字段的显式赋值(如integer data = null;)会覆盖此前的修改,最终导致null输出。
在Java中,对象实例化遵循严格且不可变的初始化顺序。以题中代码为例,当执行 new Sub(10) 时,实际发生以下步骤:
-
调用父类构造器:
Sub(int f)中super(f)触发Super(int f)执行; -
父类构造器内调用抽象方法:
Super构造器立即调用init(f)—— 此时子类对象已分配内存但尚未完成初始化,JVM 动态分派到Sub.init(10); -
子类方法提前写入字段:
Sub.init()将data = 10(自动装箱为Integer.valueOf(10)),此时data字段暂存为非null值; -
返回子类构造器,执行字段初始化语句:按规范,
Integer data = null;作为实例变量声明初始化,等价于在super(f)返回后、构造器余下代码前插入data = null;—— 直接覆盖了上一步设置的10; - 构造完成,
sub.showData()输出null。
该行为可通过反编译或调试验证:javap -c Sub 可见其构造器字节码中 aload_0; iconst_0; invokestatic java/lang/Integer.valueOf(I)Ljava/lang/Integer; putfield ...data 明确出现在 invokespecial Super.<init></init> 之后。
✅ 正确做法是禁止在构造器中调用可被重写的方法(包括 protected / public 实例方法及抽象方法)。修复方案有二:
-
推荐:延迟初始化 + 模板方法模式
Java Development Manual下载Java开发手册规约集合,基于阿里巴巴Java开发手册(嵩山版)。 涵盖7大维度:编程规约、异常日志、单元测试、安全规约、MySQL数据库、工程结构、设计规约。 当用户需要:(1) 编写或审查Java代码 (2) 检查命名/代码规范 (3) 处理异常和日志 (4) 编写单元测试 (5) 安全编码 (6) 数据库设...
立即学习“Java免费学习笔记(深入)”;
abstract class Super { protected Integer data; Super(int f) { // 不在此处调用 init() } // 提供受控的初始化入口 final void initialize(int f) { init(f); } abstract void init(int f); } class Sub extends Super { Integer data = null; // 仍存在风险,建议移除显式初始化 Sub(int f) { super(f); initialize(f); // 构造器末尾安全调用 } @Override void init(int f) { this.data = f; // 此时字段已初始化完毕,不会被覆盖 } } -
更简洁:使用
final字段 + 构造器参数传递class Sub extends Super { final Integer data; Sub(int f) { super(f); this.data = f; // 在子类构造器中明确赋值,无覆盖风险 } }
⚠️ 注意事项:
- 此问题不仅限于抽象方法,任何
public/protected实例方法在构造器中被调用均存在多态风险; - 静态字段和
final基本类型字段不受此影响,但final引用类型仍需确保构造安全; - 《Effective Java》第19条明确指出:“Constructors must not invoke overridable methods”——这是面向对象设计中保障对象不变性的基本原则。
遵循该原则,可从根本上避免“子类字段被父类构造流程意外擦除”的隐蔽bug,提升代码健壮性与可维护性。

















