慎用继承

之前看到Jake Wharton利用error prone搞了个java代码编译时检查工具,规则如下:除了使用final,abstract修饰的类外,所有类必须打上@Open注解。 言外之意不是专门被设计用来继承的类尽量用final修饰。无独有偶,kotlin中类默认是final的,想被继承需要手动加上open关键字。这都说明:这些大佬不希望类是默认可被继承的,也就是撸码过程中继承关系需要慎用。那么为什么呢?

继承打破了封装性

类A继承类B,B中一些方法的实现可能随不同版本变化而变化,有些变化可能会打破A的鲁棒性。例如我们想继承HashSet实现一个能统计历史加过多少个元素的hashSet,我们可能会重写其addAll和add方法,add的时候计数+1,同时调用父类HashSet的add方法,addAll(Collection<? extends E> c)的时候计数加上c.size(),同时调用父类HashSet的addAll方法。乍一看没什么问题。但是HashSet的addAll方法是通过add方法实现的,这就会导致调用add的时候计数正确,调用addAll的时候计数增加的不是c.size()而是两倍的c.size()。如果我们不了解addAll的实现细节,我们不容易注意到这个问题。那你可能会说那我不覆写addAll方法不就能work了吗?确实是,但是work的前提是HashSet的addAll方法一直通过add来实现。而这种实现细节没人能承诺一成不变。一旦子类的鲁棒性依赖于父类方法的实现细节这个继承关系就不太可靠,也不利于维护。而理想的封装正在于让调用方不需要关心方法实现的细节,只需要关注方法的结果。所以说这打破了封装性。实际上这种情况组合优于继承。利用包装类持有HashSet的对象来实现HashSet对应功能,同时包装类中维护一个计数,这样就不依赖于HashSet的实现细节来。一定是在两个类之间具备强烈的”is-a”关系的时候才去使用继承,Java中stack继承自vector,也是一种继承滥用。只有确认每个B都是A,才应该让B继承自A。否则考虑组合是不是更好?

要么不继承,要么就好好设计并提供文档说明

  • 被继承的类应该有文档说明其overridable方法的自用性(即调用了哪些可被覆写的方法) 虽然说好的API文档应该描述这个方法做了什么,而不用关心它具体怎么做到的。但是因为继承打破了封装性,所以我们实现继承的时候必须把overridable方法的自用性描述清楚。否则可能就是在挖坑。
  • 被设计继承的类必须提供适当的hook来让设计的protected方法进入内部工作流程 这条无需赘述,都看得懂。
  • 父类的constructor不能调用可被覆写的方法 原因很简单,父类constructor先于子类constructor执行,一旦调用可覆写的方法,子类中的变量实例很可能还没初始化,造成程序异常。同理 clone和readObject都不能调用可被覆写的方法 因为覆写的方法调用时间在反序列化和clone完成之前。
  • 尽量用接口代替抽象类。

总结来说就是,因为Java语言默认类可继承,一定程度上会加剧在工程中随意继承的现象,而随意继承会不经意间给后面维护埋下坑。并不说不要继承,而是一旦继承就好好设计并写好注释或者文档。