在研讀《深入理解 Java 虛擬機》第三部分「虛擬機執行子系統」時,第 8.3 節詳細闡述了方法調用的底層機制,其中重載與重寫在 JVM 層面的調用原理尤為關鍵。本文將結合字節碼指令,深入剖析這兩種多態形式的本質差異。
Human man = new Man(); man.foo();
上述代碼中,man 的靜態類型是 Human,實際類型是 Man。方法接收者即將執行方法的對象所有權歸屬。
| 指令 | 用途 |
|---|---|
| invokestatic | 調用靜態方法 |
| invokespecial | 調用構造器、私有方法、父類方法 |
| invokevirtual | 調用虛方法(普通實例方法) |
| invokeinterface | 調用接口方法 |
| invokedynamic | 動態解析調用點後執行 |
前兩條指令在類加載階段即可將符號引用解析為直接引用,對應的方法稱為非虛方法(靜態、構造器、私有、父類、final 方法)。雖然 final 方法使用 invokevirtual,但因無法被覆蓋,多態選擇結果唯一。
重載僅涉及靜態類型選擇。編譯器在編譯期根據參數的聲明類型確定調用哪個重載版本。
public class StaticDispatcher {
static class Human {}
static class Man extends Human {}
static class Woman extends Human {}
public void sayHello(Human human) { System.out.println("Hello guy!"); }
public void sayHello(Man man) { System.out.println("Hello man!"); }
public void sayHello(Woman woman) { System.out.println("Hello woman!"); }
public static void main(String[] args) {
StaticDispatcher sr = new StaticDispatcher();
Human man = new Man();
Human woman = new Woman();
sr.sayHello(man); // 輸出: Hello guy!
sr.sayHello(woman); // 輸出: Hello guy!
sr.sayHello((Man)man); // 輸出: Hello man!
sr.sayHello((Woman)man); // 拋出 ClassCastException
}
}
26: invokevirtual #13 // Method sayHello:(LStaticDispatcher$Human;)V
31: invokevirtual #13 // Method sayHello:(LStaticDispatcher$Human;)V
36: checkcast #9 // class StaticDispatcher$Man
39: invokevirtual #14 // Method sayHello:(LStaticDispatcher$Man;)V
44: checkcast #11 // class StaticDispatcher$Woman
47: invokevirtual #15 // Method sayHello:(LStaticDispatcher$Woman;)V
強制類型轉換會插入 checkcast 指令,且 invokevirtual 目標方法隨靜態類型變化而改變。結論:重載由編譯器根據參數靜態類型靜態綁定。
char → int → long → float → double → Character → Serializable → Object
重寫體現運行期多態,調用目標取決於對象實際類型。
public class DynamicDispatcher {
static abstract class Human { protected abstract void sayHello(); }
static class Man extends Human { @Override protected void sayHello() { System.out.println("Man say hello"); } }
static class Woman extends Human { @Override protected void sayHello() { System.out.println("Woman say hello"); } }
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello(); // Man say hello
woman.sayHello(); // Woman say hello
man = new Woman();
man.sayHello(); // Woman say hello
}
}
17: invokevirtual #6 // Method DynamicDispatcher$Human.sayHello:()V
21: invokevirtual #6 // Method DynamicDispatcher$Human.sayHello:()V
33: invokevirtual #6 // Method DynamicDispatcher$Human.sayHello:()V
字節碼中調用的均為父類 Human.sayHello,但運行結果卻根據實際類型分派到不同子類實現。這正是 invokevirtual 的動態分派機制:運行期根據對象實際類型在繼承鏈向上搜索合適方法。
若父類未聲明某方法,子類雖實現該方法,但通過父類引用調用仍會編譯報錯。這證明 Java 編譯器基於靜態類型進行檢查。
本文通過字節碼層面揭示了重載(靜態分派)與重寫(動態分派)的本質區別。下一篇將探討單分派與多分派機制。
周志明《深入理解 Java 虛擬機》