Effective Java 第三版——18. 组合优于继承

Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化。
在这里第一时间翻译成中文版。供大家学习分享之用。

Effective Java, Third Edition

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方法是基于它的add方法来实现的,即使HashSet文档中没有指名其实现细节,倒也是合理的。InstrumentedHashSet中的addAll方法首先给addCount属性设置为3,然后使用super.addAll方法调用了HashSet的addAll实现。然后反过来又调用在InstrumentedHashSet类中重写的add方法,每个元素调用一次。这三次调用又分别给addCount加1,所以,一共增加了6:通过addAll方法每个增加的元素都被计算了两次。

我们可以通过消除addAll方法的重写来“修复”子类。 尽管生成的类可以正常工作,但是它依赖于它的正确方法,因为HashSet的addAll方法是在其add方法之上实现的。 这个“自我使用(self-use)”是一个实现细节,并不保证在Java平台的所有实现中都可以适用,并且可以随发布版本而变化。 因此,产生的InstrumentedHashSet类是脆弱的。

稍微好一点的做法是,重写addAll方法遍历指定集合,为每个元素调用add方法一次。 不管HashSet的addAll方法是否在其add方法上实现,都会保证正确的结果,因为HashSet的addAll实现将不再被调用。然而,这种技术并不能解决所有的问题。 这相当于重新实现了父类方法,这样的方法可能不能确定到底是否时自用(self-use)的,实现起来也是困难的,耗时的,容易出错的,并且可能会降低性能。 此外,这种方式并不能总是奏效,因为子类无法访问一些私有属性,所以有些方法就无法实现。

导致子类脆弱的一个相关原因是,它们的父类在后续的发布版本中可以添加新的方法。假设一个程序的安全性依赖于这样一个事实:所有被插入到集中的元素都满足一个先决条件。可以通过对集合进行子类化,然后并重写所有添加元素的方法,以确保在添加每个元素之前满足这个先决条件,来确保这一问题。如果在后续的版本中,父类没有新增添加元素的方法,那么这样做没有问题。但是,一旦父类增加了这样的新方法,则很有肯能由于调用了未被重写的新方法,将非法的元素添加到子类的实例中。这不是个纯粹的理论问题。在把Hashtable和Vector类加入到Collections框架中的时候,就修复了几个类似性质的安全漏洞。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpwdsg.html