多態是物件導向三大特性之一。Java 實現兩種多態:編譯時多態 (靜態綁定) — 方法重載;運行時多態 (動態綁定) — 方法重寫。本文通過完整代碼追蹤 JVM 分派邏輯,徹底釐清兩者差異、陷阱與最佳實踐。
| 維度 | 方法重載 | 方法重寫 |
|---|---|---|
| 別名 | Overload | Override |
| 綁定時機 | 編譯期 (靜態綁定) | 運行期 (動態綁定) |
| 發生範圍 | 同一類別 | 父子類別 (繼承體系) |
| 方法簽名 | 名稱相同,**參數列表不同** (類型/個數/順序) | 名稱、參數列表、返回類型**完全相同** (协变返回除外) |
| 返回類型 | 可不同 | 兼容 (子類型/基本類型相同) |
| 拋出異常 | 無限制 | 子類異常 ⊆ 父類異常 |
| 存取修飾符 | 無限制 | 子類 ≥ 父類 (不能縮小) |
| 多態體現 | 編譯器根據實參類型選擇方法 | JVM 根據實例真實類型調用方法 |
class A {
public void print(A a) { System.out.println("A and A"); }
public void print(D d) { System.out.println("A and D"); }
}
class B extends A {
public void print(B b) { System.out.println("B and B"); }
@Override public void print(A a) { System.out.println("B and A"); }
}
class C extends B {}
class D extends C {}
A a1 = new A(); A a2 = new B(); B b = new B(); C c = new C(); D d = new D();
| 調用 | 靜態類型 | 動態類型 | 輸出 | 解析 |
|---|---|---|---|---|
a1.print(a1) | A | A | A and A | 靜態類型 A 只有 print(A)、print(D),匹配 print(A) |
a1.print(b) | A | B | A and A | **重載由靜態類型決定**:A 無 print(B),向上匹配 print(A) |
a1.print(c) | A | C | A and A | 同理,A 無 print(C),匹配 print(A) |
a1.print(d) | A | D | A and D | A 有 print(D),直接匹配 |
a2.print(b) | A | B | B and A | **關鍵**:靜態類型 A 匹配 print(A) → 動態類型 B 重寫了 print(A) → 執行 B.print(A) |
a2.print(c) | A | B | B and A | A 無 print(C) → 匹配 print(A) → B 重寫 → B.print(A) |
a2.print(d) | A | B | A and D | A 有 print(D) → B 未重寫 print(D) → 執行 A.print(D) |
a2.print(a2) | A | B | B and A | 靜態 A 匹配 print(A) → 動態 B 重寫 → B.print(A) |
b.print(b) | B | B | B and B | 靜態 B 有 print(B) → 直接匹配 |
b.print(c) | B | C | B and B | B 無 print(C) → 向上匹配 print(B) → C 未重寫 → B.print(B) |
b.print(d) | B | D | A and D | B 無 print(D) → 向上查找 A 有 print(D) → 執行 A.print(D) |
invokestatic, invokespecial, invokevirtual (但目標方法在編譯期已確定)。A a2 = new B(); a2.print(b) 中,編譯器看到 a2 是 A 類型,b 是 B 類型,直接綁定 A.print(A) (因為 A 無 print(B)),而非 B.print(B)。invokevirtual (核心)、invokeinterface。A a2 = new B(); a2.print(a2) → 編譯期綁定 A.print(A) → 運行期 invokevirtual 發現實例是 B → 查 B 方法表發現重寫 print(A) → 執行 B.print(A)。Java 單分派 (僅動態類型決定重寫)。若需「雙分派」(靜態+動態雙重決定),需手動模擬:
// Visitor 模式雙分派示例
interface Shape { void accept(Visitor v); }
class Circle implements Shape { public void accept(Visitor v) { v.visit(this); } }
class Rect implements Shape { public void accept(Visitor v) { v.visit(this); } }
interface Visitor { void visit(Circle c); void visit(Rect r); }
class DrawVisitor implements Visitor { ... }
class AreaVisitor implements Visitor { ... }
// 使用: shape.accept(drawVisitor) → 動態分派到 Circle.accept → 靜態分派到 visit(Circle)
class Parent { void process(List list) {} }
class Child extends Parent {
// 錯誤:參數 List
class Parent { private void helper() {} }
class Child extends Parent { void helper() {} } // 新方法,非重寫,無多態
class Parent { static void foo() {} }
class Child extends Parent { static void foo() {} } // 隱藏,非重寫
// Parent.foo() vs Child.foo() 由調用類型決定,無動態分派
@Override:編譯器驗證確實重寫,防止誤寫成重載。process(Object) vs process(String)) 易混淆,優先考慮不同方法名或參數對象封裝。重載 = 編譯器根據「引用類型 + 實參類型」靜態綁定 (同一類別內) 重寫 = JVM 根據「實例真實類型」動態分派 (父子類別間) 靜態類型決定「能調用哪組重載」 動態類型決定「最終執行哪個重寫版本」
徹底理解靜態/動態分派雙重機制,是掌握 Java 多態、設計模式 (Visitor/Strategy/Template Method) 以及排查「為什麼調用了錯誤方法」的關鍵鑰匙。