这篇文章的主题并不是鼓励不使用继承,而是仅从使用继承带来的问题出发,讨论继承机制不太好的地方,从而在使用时慎重选择,避开可能遇到的坑。ide
JAVA中使用到继承就会有两个没法回避的缺点:函数
关于这一点,下面是一个详细的例子(来源于Effective Java第16条)测试
public class MyHashSet<E> extends HashSet<E> { private int addCount = 0; public int getAddCount() { return addCount; } @Override public boolean add(E e) { addCount++; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); } }
这里自定义了一个HashSet
,重写了两个方法,它和超类惟一的区别是加入了一个计数器,用来统计添加过多少个元素。this
写一个测试来测试这个新增的功能是否工做:设计
public class MyHashSetTest { private MyHashSet<Integer> myHashSet = new MyHashSet<Integer>(); @Test public void test() { myHashSet.addAll(Arrays.asList(1,2,3)); System.out.println(myHashSet.getAddCount()); } }
运行后会发现,加入了3个元素以后,计数器输出的值是6。指针
进入到超类中的addAll()
方法就会发现出错的缘由:它内部调用的是add()
方法。因此在这个测试里,进入子类的addAll()
方法时,数器加3,而后调用超类的addAll()
,超类的addAll()
又会调用子类的add()
三次,这时计数器又会再加三。code
将这种状况抽象一下,能够发现出错是由于超类的可覆盖的方法存在自用性(即超类里可覆盖的方法调用了别的可覆盖的方法),这时候若是子类覆盖了其中的一些方法,就可能致使错误。对象
好比上图这种状况,Father
类里有可覆盖的方法A
和方法B
,而且A
调用了B
。子类Son
重写了方法B
,这时候若是子类调用继承来的方法A
,那么方法A
调用的就再也不是Father.B()
,而是子类中的方法Son.B()
。若是程序的正确性依赖于Father.B()
中的一些操做,而Son.B()
重写了这些操做,那么就极可能致使错误产生。blog
关键在于,子类的写法极可能从表面上看来没有问题,可是却会出错,这就迫使开发者去了解超类的实现细节,从而打破了面向对象的封装性,由于封装性是要求隐藏实现细节的。更危险的是,错误不必定能轻易地被测出来,若是开发者不了解超类的实现细节就进行重写,那么可能就埋下了隐患。继承
这一点比较好理解,主要有如下几种可能:
设计能够用来继承的类时,应该注意:
详细解释下第三点。它实际上和 继承打破了封装性 里讨论的问题很类似,假设有如下代码:
public class Father { public Father() { someMethod(); } public void someMethod() { } }
public class Son extends Father { private Date date; public Son() { this.date = new Date(); } @Override public void someMethod() { System.out.println("Time = " + date.getTime()); } }
上述代码在运行测试时就会抛出NullPointerException
:
public class SonTest { private Son son = new Son(); @Test public void test() { son.someMethod(); } }
由于超类的构造函数会在子类的构造函数以前先运行,这里超类的构造函数对someMethod()
有依赖,同时someMethod()
被重写,因此超类的构造函数里调用到的将是Son.someMethod()
,而这时候子类还没被初始化,因而在运行到date.getTime()
时便抛出了空指针异常。
所以,若是在超类的构造函数里对可覆盖的方法有依赖,那么在继承时就可能会出错。
继承有不少优势,但使用继承时应该慎重并多加考虑。一样用来实现代码复用的还有复合,若是使用继承和复合皆可(这是前提),那么应该优先使用复合,由于复合能够保持超类对实现细节的屏蔽,上述关于继承的缺点均可以用复合来避免。这也是所谓的复合优先于继承。
若是使用继承,那么应该留意重写超类中存在自用性的可覆盖方法可能会出错,即便不进行重写,超类更新时也可能会引入错误。同时也应该精心设计超类,对任何相互调用的可覆盖方法提供详细文档。