类DS字段声明时初始化:在构造函数调用前还是后?
这个坑我之前踩过好几次!太懂这种明明觉得字段应该初始化了结果还是NPE的崩溃感了,咱们来理清楚到底咋回事:
你认知里的“声明时初始化的类字段在构造函数调用前完成”其实没错,但这里的“构造函数”特指子类自己的构造函数,而Java对象的初始化顺序是有严格规则的:
- 先执行父类的静态变量/静态代码块(如果有的话)
- 执行子类的静态变量/静态代码块
- 初始化父类的实例变量
- 执行父类的构造函数
- 初始化子类的实例变量
- 执行子类的构造函数
回到你的场景:当PreConcreteClass调用super()完成父类构造后,不管是PreConcreteClass的构造逻辑里调用了update,还是父类构造中已经间接调用了,此时ConcreteClass的internalDS还没到初始化的步骤(步骤5要等父类构造全走完才会执行),所以它还是默认值null,这时候进入update的for循环,必然触发NPE。
给你补个简化的代码示例,一看就明白:
abstract class Parent { public Parent() { // 父类构造中调用了可被重写的方法 doUpdate(); } protected abstract void doUpdate(); } class Middle extends Parent { public Middle() { super(); // 这里如果也调用doUpdate,同样会触发问题 } } class Child extends Middle { // 声明时初始化,但父类构造执行时还没走到这一步 private List<String> internalDS = new ArrayList<>(); @Override protected void doUpdate() { // 此时internalDS是null! for (String s : internalDS) { // ... } } }
核心思路就是避免在构造函数链中调用子类可重写的方法,同时调整初始化与方法调用的顺序,这里有几个可行的方案:
重构构造逻辑,移除构造中的可重写方法调用
这是最根本的解决方案。把原本在构造中调用update的逻辑抽出来,比如新增一个init()方法,要求对象创建完成后手动调用;或者用工厂方法来创建实例,确保构造全完成后再执行初始化逻辑:// 工厂方法示例 public static ConcreteClass createInstance() { ConcreteClass instance = new ConcreteClass(); instance.update(); // 此时字段已经初始化完成 return instance; }在update方法中做null安全检查
作为临时修复,可以在update里先判断字段是否初始化,第一次调用时完成初始化:@Override protected void update() { if (internalDS == null) { internalDS = new ArrayList<>(); } for (String s : internalDS) { // ... } }不过这只是治标,还是建议从构造调用链上彻底解决问题。
将字段改为静态变量(谨慎使用)
如果internalDS是类级别的共享数据,可以改成静态变量,它的初始化时机是在类加载时(早于任何构造函数执行),但要注意静态变量的线程安全和复用性问题,不是所有场景都适用。
内容的提问来源于stack exchange,提问作者Rann Lifshitz

