Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化。
在这里第一时间翻译成中文版。供大家学习分享之用。
18. 组合优于继承
继承是实现代码重用的有效方式,但并不总是最好的工具。使用不当,会导致脆弱的软件。 在包中使用继承是安全的,其中子类和父类的实现都在同一个程序员的控制之下。对应专门为了继承而设计的,并且有文档说明的类来说(条目 19),使用继承也是安全的。 然而,从普通的具体类跨越包级边界继承,是危险的。 提醒一下,本书使用“继承”一词来表示实现继承(当一个类继承另一个类时)。 在这个项目中讨论的问题不适用于接口继承(当类实现接口或当接口继承另一个接口时)。
与方法调用不同,继承打破了封装[Snyder86]。 换句话说,一个子类依赖于其父类的实现细节来保证其正确的功能。 父类的实现可能会从发布版本不断变化,如果是这样,子类可能会被破坏,即使它的代码没有任何改变。 因此,一个子类必须与其超类一起更新而变化,除非父类的作者为了继承的目的而专门设计它,并对应有文档的说明。
为了具体说明,假设有一个使用HashSet
的程序。 为了调整程序的性能,需要查询HashSe
,从创建它之后已经添加了多少个元素(不要和当前的元素数量混淆,当元素被删除时数量也会下降)。 为了提供这个功能,编写了一个HashSet
变体,它保留了尝试元素插入的数量,并导出了这个插入数量的一个访问方法。 HashSet
类包含两个添加元素的方法,分别是add
和addAll
,所以我们重写这两个方法:
// Broken - Inappropriate use of inheritance! public class InstrumentedHashSet<E> extends HashSet<E> { // The number of attempted element insertions private int addCount = 0; public InstrumentedHashSet() { } public InstrumentedHashSet(int initCap, float loadFactor) { super(initCap, loadFactor); } @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); } public int getAddCount() { return addCount; } }
这个类看起来很合理,但是不能正常工作。 假设创建一个实例并使用addAll
方法添加三个元素。 顺便提一句,请注意,下面代码使用在Java 9中添加的静态工厂方法List.of
来创建一个列表;如果使用的是早期版本,请改为使用Arrays.asList
:
InstrumentedHashSet<String> s = new InstrumentedHashSet<>(); s.addAll(List.of("Snap", "Crackle", "Pop"));
我们期望getAddCount
方法返回的结果是3,但实际上返回了6。哪里出来问题?在HashSet
内部,addAll