繼承雖強大(代碼復用、多態、擴展),卻可能破壞「封裝」——軟體設計第一原則。本文通過具體代碼演示繼承如何導致父子類產生「實現細節耦合」,並提供三大避免策略與正確使用指南。
// 父類
public class Base {
private int[] arr = new int[1000];
private int count;
public void add(int n) { if (count < 1000) arr[count++] = n; }
public void addAll(int[] nums) { for (int n : nums) add(n); } // 內部調用 add()
}
// 子類:重寫 add/addAll 並統計總和
public class Child extends Base {
private long sum;
@Override public void add(int n) { super.add(n); sum += n; }
@Override public void addAll(int[] nums) { super.addAll(nums); for (int n : nums) sum += n; } // 重複累加!
public long getSum() { return sum; }
}
測試:new Child().addAll(new int[]{1,2,3}),預期 sum=6,實際輸出 12。原因:父類 addAll 循環調用 add,動態綁定觸發子類 add,導致雙重累加。
修正子類 addAll 移除循環累加後,父類若將 addAll 改為不調用 add(直接寫入陣列),子類 sum 又會變 0。結論:子類依賴父類實現細節,父類內部重構即破壞子類。
// 父類新增 clear()
public void clear() { for (int i=0;i<count;i++) arr[i]=0; count=0; }
// 子類未重寫 clear(),調用者呼叫 c.clear() 後再 addAll,sum 未歸零,結果錯誤
父類隨意新增方法,等於強迫所有子類繼承,子類若不覆寫即可能狀態不一致。
fly(),企鵝繼承鳥類卻不會飛。若企鵝重寫 fly() 實現游泳,違反父類契約,導致以父類引用操作子類時行為混亂。final class 禁止繼承,final method 禁止重寫,保留父類修改自由。| 場景 | 注意事項 |
|---|---|
| 擴展第三方庫類 | 僅重寫必要方法;避免依賴私有/包私有細節;閱讀文檔了解方法調用鏈。 |
| 編寫基類供他人繼承 | 嚴守 is-a;核心 public 方法加 final;文檔化可重寫方法契約;變更時發布遷移說明。 |
| 基類/子類自行控制 | 可適當放寬,但仍建議遵循上述原則,降低未來維護成本。 |
繼承是雙刃劍:威力巨大卻暗藏封裝破壞風險。優先考慮 final、組合、介面;必須繼承時,嚴守契約、文檔化、謹慎變更。下一節將深入介面機制,展示如何以契約取代繼承實現多態。
