JVM學習筆記(二)--方法調用之靜態分配和動態分配
本篇文章從JVM的角度來理解Java學習中經常提到的重載和重寫。
方法調用:方法調用不等同於方法執行,在Java虛擬機中,方法調用僅僅是為了確定調用哪個版本的方法。方法調用分為解析調用和分派。解析調用一定是靜態的,而分派可以是靜態的,也可以是動態的。我們這裏只介紹分派中的靜態分配和動態分配。
- 靜態分配:所有依賴靜態類型來定位方法執行版本的分派動作稱為靜態分配。
下面看個例子,順便來猜一下結果(面試中經常遇到):
1 class Human { 2 3 } 4 5 class Man extends Human{ 6 7 } 8 9 classWoman extends Human { 10 11 } 12 13 public class overLoadTest { 14 15 public void sayHello(Human guy){ 16 System.out.println("Hello guy!"); 17 } 18 public void sayHello(Man man){ 19 System.out.println("Hello man!"); 20 } 21 public void sayHello(Woman woman){22 System.out.println("Hello woman!"); 23 } 24 25 26 public static void main(String[] args){ 27 Human man = new Man(); 28 Human woman = new Woman(); 29 30 overLoadTest sr = new overLoadTest(); 31 32 sr.sayHello(man); 33 sr.sayHello(woman);34 35 } 36 }
運行結果如下:
如果不明白為什麽是這樣的結果,我們先來了解一下,什麽是靜態類型和實際類型。
Human man = new Man();
Human woman = new Woman();
這裏Human為靜態類型,man和woman為實際類型。區別為靜態類型的變化僅僅在使用時發生,變量本身的靜態類型不會改變,並且最終的靜態類型是在編譯期間可知的;而實際類型變化的結果在運行期間才可確定,編譯器在編譯程序時並不知道一個對象的實際類型是什麽。
靜態分配最典型的應用就是方法重載。我們再來復習一下方法重載的定義:在一個類中有多個方法,名稱相同但參數列表不同(參數個數,參數類型,參數順序)。上面例子就是方法重載。這裏的運行結果毫不意外。
sr.sayHello(man);
sr.sayHello(woman);
man和woman的靜態類型都是Human。
-
動態分配 :所有依賴實際類型來定位方法執行版本的分派動作稱為靜態分配。
動態分配的典型應用為重寫。下面為重寫的例子
1 abstract class Human_Test { 2 public abstract void sayHello(); 3 } 4 5 class Man_Test extends Human_Test{ 6 @Override 7 public void sayHello() { 8 System.out.println("Hello man!"); 9 } 10 11 } 12 13 class Woman_Test extends Human_Test { 14 @Override 15 public void sayHello() { 16 System.out.println("Hello woman!"); 17 } 18 19 } 20 21 public class OverrideTest { 22 public static void main(String[] args){ 23 Human_Test man = new Man_Test(); 24 Human_Test woman = new Woman_Test(); 25 26 man.sayHello(); 27 woman.sayHello(); 28 29 man = new Woman_Test(); 30 man.sayHello(); 31 } 32 33 34 35 }
運行結果:
因為動態分配是根據實際類型來確定要調用的方法的,這樣的結果也比較符合我們的思維習慣。那麽JVM又是怎麽根據實際類型來找到目標方法的呢?用Javap來分析一下字節碼:
看上圖17和21行,可以看到已經找到了Human中的sayHello(),但是invokevirtual指令是多態查找指令。其過程如下:
1). 找到操作數棧頂的第一個元素所指向的對象的實際類型,記作C.
2). 如果在類型C中找到與常量池中描述符和簡單名稱都相符的方法,則進行訪問權限的校驗,如果校驗不通過,則返回java.lang.IllegaAccessError異常,校驗通過則直接返回方法的直接引用,查找過程結束。
3). 否則,按照繼承關系從下往上一次對C的各個父類進行第二步驟的搜索和驗證過程。
4). 如果始終還是沒有找到合適的方法直接引用,則拋出java.lang.AbstractMethodError異常。
由於invokevirtual指令執行的第一步是在運行時確定接收者的實際類型,所以兩次中的invokevirtual指令把常量池中的類方法符號引用解析到不同的直接引用上,這個就是java語言中方法重寫的本質。
JVM學習筆記(二)--方法調用之靜態分配和動態分配