繼承 是面向對象復用代碼的核心機制之一,但也是最容易被濫用、導致強耦合的特性。本文系統講解 extends 語法、衍生類對象內存佈局、protected 訪問控制、方法覆蓋、構造器鏈調用、super 與 this 語義、以及「組合優於繼承」設計原則對比,助你在正確場景下正確使用繼承。
class Human {
private int height;
public Human(int h) { this.height = h; }
public int getHeight() { return height; }
public void growHeight(int h) { height += h; }
public void breath() { System.out.println("hu...hu..."); }
}
class Woman extends Human { // 單繼承
public Woman(int h) { super(h); } // 必須顯式調用父構造器
public Human giveBirth() {
System.out.println("Give birth");
return new Human(20);
}
@Override
public void breath() {
super.breath(); // 調用父類實現
System.out.println("su...");
}
}
┌─────────────────────────────────────┐ │ Woman Object (衍生類) │ ├─────────────────────────────────────┤ │ Human Part (基類對象 subobject) │ ← super 引用此區域 │ ├─ private int height (紅×××) │ ← 衍生類不可訪問,外部不可訪問 │ ├─ public getHeight() (綠✓) │ ← 衍生類可訪問,外部可訪問 │ ├─ public breath() (綠✓) │ ← 可覆蓋 │ └─ ... │ ├─────────────────────────────────────┤ │ Woman Part (衍生層) │ ← this 引用整個對象 │ ├─ public giveBirth() (綠✓) │ ← 衍生類新增 │ ├─ public breath() (綠✓) │ ← 覆蓋版本 │ └─ ... │ └─────────────────────────────────────┘
new Woman() 先在堆上分配 Human 部分,再擴展 Woman 部分private 只在聲明類可見;protected 在聲明類 + 衍生類可見;public 普遍可見super 視圖僅含基類部分;this 視圖含完整衍生對象class Base {
protected int protectedField = 1;
private int privateField = 2;
public int publicField = 3;
protected void protectedMethod() {}
private void privateMethod() {}
}
class Derived extends Base {
void test() {
System.out.println(protectedField); // ✅ 可訪問
System.out.println(publicField); // ✅ 可訪問
// System.out.println(privateField); // ❌ 編譯錯誤
protectedMethod(); // ✅ 可調用
// privateMethod(); // ❌ 不可見
}
}
設計意圖:封裝「衍生類需知、外部不可知」的內部實現細節。
| 規則 | 說明 | 代碼示例 |
|---|---|---|
| 簽名相同 | 方法名、參數列表、返回類型 (协变返回允許子類型) | @Override 編譯器校驗 |
| 權限不降 | 子類權限 ≥ 父類權限 | 父 protected → 子 public ✅;父 public → 子 protected ❌ |
| 異常不增 | 子類拋出異常 ⊆ 父類異常 | 父 throws IOException → 子 throws FileNotFoundException ✅ |
| 不可覆蓋 | final/static/private/構造器 | final 鎖定實現,static 隱藏非覆蓋 |
class Human {
private int height;
public Human(int h) { this.height = h; System.out.println("Human init"); }
}
class Woman extends Human {
private String name;
public Woman(int h, String name) {
super(h); // 必須第一行,且只能調用一次
this.name = name; // 衍生層字段初始化
System.out.println("Woman init");
}
}
// new Woman(160, "Alice") 輸出:
// Human init
// Woman init
機制:JVM 保證基類構造器先完成,衍生類字段才初始化。若父類無無參構造器,子類必須顯式 super(...)。
| 維度 | 繼承 | 組合 |
|---|---|---|
| 關係 | Is-A (是一種) | Has-A (有一個) |
| 耦合度 | 強:子類依賴父類實現細節 | 弱:依賴介面契約 |
| 封裝性 | 破壞:protected 暴露內部 | 保護:內部對象完全隱藏 |
| 靈活性 | 編譯期靜態綁定 | 運行期動態替換組件 |
| 多態 | 天然支援 | 需配合介面實現 |
| 構造順序 | 父→子固定 | 完全可控 |
| 適用場景 | 「確實是」關係穩定、需複用父類邏輯、擴展點固定 | 大多數復用場景、需動態行為、多組件組合 |
// ❌ 錯誤:Stack 繼承 Vector (Java 早期失誤)
// Stack 是 Vector 嗎?不是,Stack 是「棧」行為,Vector 是「動態數組」實現
// 導致 Stack 繼承了 Vector 所有 public 方法 (add/insert/removeAt 等),破壞棧語義
// 正確:Stack 組合 List/Deque 內部委派
// ❌ 錯誤:Properties 繼承 Hashtable
// Properties 是「鍵值配置」,Hashtable 是「同步雜湊表」
// 導致 Properties 可 put 非 String 鍵值,getProperty 卻只取 String
// 正確:Properties 組合 Map
interface ServiceService delegate 字段super.method() 改為 delegate.method(),暴露需對外的方法extends 是強大的「Is-A」建模工具,但會產生強耦合、破壞封裝、固化層次。掌握 衍生類內存佈局、protected 語義、覆蓋規則、構造器鏈機制 是正確使用前提。工程實踐中 默認選擇組合,僅在「確實是穩定 Is-A 關係、需複用完整父類邏輯、擴展點固定」時使用繼承。記住 GoF 經典原則:Favor composition over inheritance。下一篇我們將深入 Java 介面默認方法與函數式介面新特性。