继承与组合:设计选择的权衡
立即解锁
发布时间: 2025-08-18 00:25:49 阅读量: 1 订阅数: 4 

# 继承与组合:设计选择的权衡
## 1. 继承并非总是最佳选择
继承是实现代码复用的强大方式,但并非适用于所有场景。不恰当使用继承会导致软件变得脆弱。在以下两种情况下使用继承相对安全:
- 在同一个包内,子类和父类的实现由同一批程序员控制。
- 扩展专门为继承而设计和文档化的类。
然而,跨包继承普通的具体类是危险的。这里所说的继承指的是实现继承(一个类扩展另一个类),而接口继承(一个类实现一个接口或一个接口扩展另一个接口)不受这些问题的影响。
### 1.1 继承违反封装性
与方法调用不同,继承违反了封装性。子类的正常功能依赖于父类的实现细节,而父类的实现可能会在不同版本中发生变化,即使子类的代码未作修改,也可能会因此而崩溃。除非父类的作者专门为扩展目的设计并文档化了该类,否则子类必须与父类同步发展。
### 1.2 示例:错误使用继承的 `InstrumentedHashSet`
假设我们有一个使用 `HashSet` 的程序,为了调优程序性能,需要查询 `HashSet` 自创建以来添加的元素数量。我们编写了一个 `InstrumentedHashSet` 类来实现这个功能:
```java
// 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
InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
```
我们期望 `getAddCount` 方法返回 3,但实际上返回 6。这是因为 `HashSet` 的 `addAll` 方法是基于 `add` 方法实现的,而 `InstrumentedHashSet` 的 `addAll` 方法在增加计数后调用了 `HashSet` 的 `addAll` 实现,这又会调用 `InstrumentedHashSet` 重写的 `add` 方法,导致每个元素被重复计数。
### 1.3 解决继承问题的尝试及局限性
我们可以尝试通过消除对 `addAll` 方法的重写来“修复”子类,但这会使子类的正常功能依赖于 `HashSet` 的 `addAll` 方法基于 `add` 方法实现这一细节,而这是一个可能会在不同版本中改变的实现细节,因此修复后的类仍然很脆弱。
另一种方法是重写 `addAll` 方法,遍历指定的集合,为每个元素调用一次 `add` 方法。这种方法可以保证结果的正确性,但它相当于重新实现父类的方法,可能会导致自我使用,而且困难、耗时且容易出错。此外,由于某些方法的实现需要访问子类无法访问的私有字段,这种方法并不总是可行。
### 1.4 子类脆弱性的其他原因
子类的另一个脆弱性原因是父类可能在后续版本中添加新方法。如果一个程序的安全性依赖于集合中插入的所有元素都满足某个谓词,通过子类化集合并重写每个能够添加元素的方法来确保在添加元素之前满足该谓词。但当父类在后续版本中添加了一个能够插入元素的新方法时,就可能会添加“非法”元素,因为子类没有重写这个新方法。
即使只是添加新方法而不重写现有方法,扩展类也并非完全没有风险。如果父类在后续版本中添加了一个新方法,而子类恰好有一个具有相同签名但不同返回类型的方法,子类将无法编译。如果子类的方法与父类的新方法具有相同的签名和返回类型,那么就相当于重写了该方法,会面临上述两个问题。
## 2. 组合与转发:更好的选择
幸运的是,有一种方法可以避免上述所有问题,即使用组合和转发。不扩展现有类,而是给新类一个私有字段,该字段引用现有类的一个实例。这种设计称为组合,因为现有类成为新类的一个组件。新类中的每个实例方法调用包含的现有类实例的相应方法并返回结果,这称为转发,新类中的方法称为转发方法。
### 2.1 `InstrumentedSet` 示例
以下是使用组合和转发方法替换 `InstrumentedHashSet` 的示例:
```java
// Wrapper class - uses composition in place of inheritance
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@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;
}
}
// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
public void clear() { s.clear(); }
public boolean contains(Object o) { return s.contains(o); }
public boolean isEmpty() { return s.isEmpty(); }
public int size() { return s.size(); }
public Iterator<E> iterator() {
```
0
0
复制全文
相关推荐










