多態是面向對象三大支柱中最具威力、也最易混淆的一環。本文從「三大前提」切入,通過代碼示例、調用規則表、內存模型圖解,徹底講透多態的本質與實戰規則,助你告別「父類引用指向子類對象後調用誰」的困惑。
| 轉型方向 | 語法 | 安全性 | 用途 |
| 向上 | Parent p = new Child(); | 自動、安全 | 多態參數傳遞、集合存儲、接口編程 |
| 向下 | Child c = (Child) parentRef; | 需強制、可能拋 ClassCastException | 調用子類特有方法/字段 |
最佳實踐:向下轉型前用 instanceof 守護:if (animal instanceof Bird) ((Bird) animal).fly();。
方法調用鐵律:編譯看左邊,運行看右邊
class Animal { void eat() { sysout("動物吃東西"); } }
class Bird extends Animal { void eat() { sysout("鳥吃蟲子"); } void fly() { sysout("飛"); } }
Animal a = new Bird();
a.eat(); // 輸出:「鳥吃蟲子」—— 運行期動態綁定到 Bird.eat()
a.fly(); // 編譯錯誤!Animal 無 fly() 方法
三大調用規則總結表
| 場景 | 調用哪個方法? | 原理 |
| 父類有方法,子類覆蓋 | 子類方法 | 動態綁定:JVM 根據實際對象類型查找方法表 |
| 父類有方法,子類未覆蓋 | 父類方法 | 繼承鏈向上查找,找到首個實現 |
| 子類特有方法(父類無) | 無法直接調用 | 編譯期類型無此方法,需向下轉型 |
內存模型視角:為什麼能動態綁定?
- 每個類加載時,方法區生成方法表,記錄方法簽名 → 字節碼入口地址。
- 子類繼承父類方法表,覆蓋方法時替換入口地址指向子類字節碼。
- 對象頭包含指向類元數據的指針(Klass Point)。
- 調用
a.eat() 時:
① 讀取對象頭指針定位到 Bird 類元數據
② 在方法表查找 eat 入口
③ 跳轉執行 Bird.eat() 字節碼
字段不具備多態:靜態綁定
class Parent { int x = 1; }
class Child extends Parent { int x = 2; }
Parent p = new Child();
System.out.println(p.x); // 輸出 1!字段按編譯期類型綁定(Parent.x)
結論:只有非 static、非 final、非 private的實例方法具有多態性。字段、靜態方法、私有方法、構造器均為靜態綁定。
實戰陷阱:構造器中調用覆蓋方法
class Base {
Base() { init(); } // 構造器調用
void init() { sysout("Base init"); }
}
class Derived extends Base {
int x = 10;
Derived() { sysout("Derived init, x=" + x); }
@Override void init() { sysout("Derived init, x=" + x); } // x 尚未初始化!
}
new Derived();
/* 輸出:
Derived init, x=0 ← 危險!字段默認值
Derived init, x=10
*/
鐵律:構造器中不可調用可覆蓋方法(直接或間接)。將方法標記 final 或 private,或移至初始化塊/工廠方法中。
總結
多態本質:同一接口,多種實現,運行期動態綁定。掌握三大前提、調用鐵律、字段無多態、構造器陷阱,即可在設計中正確應用「面向接口編程」、「策略模式」、「模板方法模式」等經典模式。記住:Animal a = new Bird() 時,a 能做的只有 Animal 定義的事,但怎麼做由 Bird 決定。
- 上一篇文章: Java 實現微信紅包算法:隨機分佈、最小值保障、總額精確控制完整代碼
- 下一篇文章: Java 類加載全流程深度解析:加載、驗證、準備、解析、初始化五階段詳解
Copyright ©
程式碼之家 All Rights Reserved