Java属性的实例化、构造函数的执行是有先后顺序的。 此外 当出现子类继承情况时,子类和父类也是有初始化顺序的,这让情况更加复杂! 下面我们重点看几个常见的构造函数陷阱!
public class Context{ private A a; private B b; }属性 a 和 b我们认为他们是“平行的”。现在他们没有存在互相依赖
Public class Context{ Private A a = new A(); Private B b = new B(a); }现在呢,可以认为 b 平行依赖于 a 。这就是对象属性间的平行依赖。当前这种情形是没有问题的。下面的场景有问题!
Public class Context{ Private A a; Private B b = new B(a); //此时B如果实例化,那么a为null Public Context(A a){ This.a = a; } }
错误出现了,之所以B接受了一个null值,是因为属性 b 的实例化要优先于构造方法的。至于NullPointerException什么时候触发,没人能预测。如果是在B构造方法中触发空指针异常,你可能会恍然大悟,”原来传入了一个null值“。但是如果是在B的常规方法调用触发,你可能需要花点时间来排查空指针异常的原因了。
有些人喜欢在声明属性的同时进行初始化,并且还设成了final常量。这样的编码方式让人感到舒适,代码的可读性也相对较高。然而,当代码变得复杂,存在"对象属性的平行依赖"的情况时,就需要小心了!
对于这一点,C++的规范做得很好,属性声明时不能进行初始化,只能在构造函数中进行初始化。所以我们经常看到C++中的构造函数非常冗长,只做了简单的赋值操作。
Public class A{ Public A(){ ....//初始化操作 function(); }
如果A的子类重写了function方法,那么A类构造方法执行的就是其子类的function实现,如果A类设计时没有考虑到这种情况,那么A的初始化就存在很大风险。所以要将 function 设为 private,或 final。
建议将存在子类重写需求的逻辑抽象出一个方法,设置为 protected 或者 public 方法设置为 final。避免子类重写 public 方法。提供给子类覆盖的方法设置为 protected,更加清晰。
下面还会讲解构造方法中 执行 public 方法有多坑。
Public class Context{ Public Context(){//构造方法 .... Register(); .... } Public void register(){ beforeRegister(); ..... afterRegister(); } Protected void beforeRegister(){ } Protected void afterRegister(){ } }
现在 Context 需要向外提供注册功能.但是实例化时,需要先注册一些服务。注册操作前后会 执行 before 方法和 after 方法,子类可以重写,向提供扩展注册功能。当子类重写了 before,after 功能,烦人的空指针异常又出现了。
子类重写了注册的before和after方法,为了监听注册功能,在子类属性中维护了额外的数据结构。但是正是子类属性触发了空指针异常。原因是父类的构造方法中执行了子类重写后的方法,子类重写的方法中使用了自己的属性。因为这个属性还未初始化,所以出现了空指针!
最简单的思路是将子类中的变量map声明为static,这就先于父类构造方法执行。这的确能解决这个bug,但引入新的bug。static变量属于整个类。不单单属于一个对象。那意味着所有的对象实例都共用这个static map, 这不是正确的逻辑。并且static map 在该进行垃圾回收时无法被回收。没准哪个时刻出现了 Out of Memory 内存溢出,服务器宕机,然后查到了这个root 根节点大对象正是static map.导致不可挽回的但本可避免的过失。
所以为什么有人一直强调,谨慎使用继承,优先使用委托。这就是原因!
面向对象的设计中,子类可以访问到父类方法,属性,但是父类无法访问子类属性,本应该是子类依赖于父类。但是 设计模式中模板方法 的思路带来了父类对子类的依赖。(模板方法:父类的方法中调用了方法A,但是方法A由子类具体实现)模板设计虽然由父类定义了逻辑处理的流程,子类只是填空。但是父类的处理逻辑中包括了不可预测的子类实现。