繼承破壞封裝,組合實現復用。本文通過 ForwardingSet + InstrumentedSet 實例,展示如何用「轉發類 + 包裝類」替代繼承,避免父類內部調用邏輯被子類覆寫導致的不可預期行為。
addAll() 內部循環調用 add()),導致子類邏輯多次執行或狀態不一致。public class ForwardingSet<E> implements Set<E> {
private final Set<E> s; // 組合:持有被包裝 Set
public ForwardingSet(Set<E> s) { this.s = s; }
// 所有方法單純轉發,無業務邏輯
@Override public int size() { return s.size(); }
@Override public boolean isEmpty() { return s.isEmpty(); }
@Override public boolean contains(Object o) { return s.contains(o); }
@Override public Iterator<E> iterator() { return s.iterator(); }
@Override public Object[] toArray() { return s.toArray(); }
@Override public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override public boolean add(E e) { return s.add(e); }
@Override public boolean remove(Object o) { return s.remove(o); }
@Override public boolean containsAll(Collection<?> c) { return s.containsAll(c); }
@Override public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
@Override public boolean retainAll(Collection<?> c) { return s.retainAll(c); }
@Override public boolean removeAll(Collection<?> c) { return s.removeAll(c); }
@Override public void clear() { s.clear(); }
}
關鍵點: s 為 private final,外部不可見,父類內部調用鏈被切斷。
super.xxx() 轉發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); // 轉發給 ForwardingSet → 內部 Set
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() { return addCount; }
}
Set<String> s = new InstrumentedSet<>(new HashSet<>());
s.add("A");
s.addAll(Arrays.asList("B", "C"));
System.out.println(((InstrumentedSet<String>) s).getAddCount()); // 輸出 3
| 維度 | 繼承 | 組合 + 包裝類 |
|---|---|---|
| 封裝性 | 破壞(子類依賴父類實現細節) | 保護(內部對象不可見) |
| 耦合度 | 強(父類變更波及子類) | 弱(只依賴介面契約) |
| 靈活性 | 低(靜態綁定) | 高(運行時動態替換被包裝對象) |
| 適用場景 | 真正的 is-a 關係 | 幾乎所有「想復用功能」的場景 |
「除非確定是 is-a,否則用組合:寫個轉發類實現介面,再寫個包裝類繼承轉發類加邏輯。」