Java的继承和组合
前言
继承破坏封装
Java有个三原则:封装、继承、多态。封装就是对外隐藏实现细节,提供简化接口。使用者不需要关心内部是怎么实现的,而且内部的实现细节是可以随时修改的,而不影响使用者。
继承可能破坏封装是因为子类和父类可能存在着实现细节的依赖。导致子类在继承父类的时候,往往不得不关心父类的实现细节,而父类在修改其实现细节的时候,如果不考虑子类,也往往会影响子类。
封装是如何被破坏的
考虑以下基类Base:
1 | public class Base { |
子类继承Base类,并在添加数字的时候汇总数字。
1 | public class Child extends Base { |
已上子类存在个大问题,addAll在实现的时候,是通过先调用基类的addAll方法,再对数字进行汇总,殊不知基类的addAll方法的实现是通过调用add方法来实现的,由于Java对象的多态性,本次调用的add方法实际是调用Child类的,因此在问题1处已经对数字进行过汇总了,而问题2处的汇总是重复的。
以上问题是继承破坏了封装的典型例子,子类本来在实现的时候,原则上是不需要关心父类的实现细节的,但是由于父类add和addAll的依赖关系,破坏了封装,需要程序设计者去阅读父类文档和源码。
解决问题
方法一:使用final避免继承
给方法加上final关键字,父类就保留了随意修改这个方法内部实现的自由,不用担心方法引用指向一个非预期的子类方法。
给类加上final关键字,父类就 保留了随意修改这个类实现的自由,使用者也不用担心,一个父类引用的变量,实际指向的却是一个完全不符合预期行为的子类对象。
方法二:优先使用组合而非继承
使用组合可以抵挡父类变化对子类的影响,从而包含子类。示例如下:
1 | public class Child { |
这样子类就不需要关心父类的实现细节了,父类修改实现也不会影响到子类了。但是这样存在一个问题,这样的子类,不能当做基类统一处理了。解决办法是使用接口,二者共同实现同一个接口。
方法三:正确使用继承
如果要使用继承,怎么正确使用呢?使用继承大概主要有三种场景:
- 基类是别人写的,我们写子类;
- 我们写基类,别人可能写子类;
- 基类、子类都是我们写的。
第1种场景中,基类主要是Java API、其他框架或类库中的类,在这 种情况下,我们主要通过扩展基类,实现自定义行为,这种情况下需要注意的是:
- 重写方法不要改变预期的行为;
- 阅读文档说明,理解可重写方法的实现机制,尤其是方法之间的 依赖关系;
- 在基类修改的情况下,阅读其修改说明,相应修改子类。
第2种场景中,我们写基类给别人用,在这种情况下,需要注意的是:
- 使用继承反映真正的is-a关系,只将真正公共的部分放到基类;
- 对不希望被重写的公开方法添加final修饰符;
- 写文档,说明可重写方法的实现机制,为子类提供指导,告诉子类应该如何重写;
- 在基类修改可能影响子类时,写修改说明。
第3种场景,我们既写基类也写子类,关于基类,注意事项和第2种场景类似,关于子类,注意事项和第1种场景类似,不过程序都由我们控制,要求可以适当放松一些。