JVM之方法調用-分派

說明:這兩天遇到的一些Java方法分派的問題,結合自己書上看的,google的,還有撒迦教我的,做一個總結吧.望指正.

 

寫道
方法分派指的是虛擬機如何確定應該執行哪個方法!

 

很多的內容可以參加撒迦的這篇博文 : http://rednaxelafx.iteye.com/blog/652719

我這篇裏很多概念的解釋都摘自上面的博文,所以,我就不一一指出啦.在此感謝撒迦的幫助.

還有一些講解(包括代碼)來自 <深入JAVA虛擬機-jvm高級特性與最佳實踐>,很好一本書,推薦對jvm有興趣的同學購買

另外 http://hllvm.group.iteye.com/group/topic/27064 這個帖子也可能幫助大家對方法分派有所瞭解.

1 靜態分派

    對於這個靜態分派,撒迦很喜歡叫做非虛方法分派(當然按照他自己的說法就是:我不喜歡叫靜態分派).

    首先,解釋一下什麼叫虛方法.虛方法的概念有點難說,不過把什麼是非虛方法說明一下,其他的就是虛方法啦.哈哈

 
非虛方法是所有的類方法(也就是申明爲static的方法) + 所有聲明爲final或private的實例方法.

 

    由於非虛方法不能被override,所以自然也不會產生子類複寫的多態效果.這樣的話,方法被調用的入口只可能是一個.而且編譯器可知.也就是說,jvm需要執行哪個方法是在編譯器就已經確定.且在運行期不會變化.很具體的例子就是方法的重載.

    看如下例子,摘自 <深入JAVA虛擬機-jvm高級特性與最佳實踐>

 

Java代碼  收藏代碼
  1. public class StaticDispatch {  
  2.       
  3.     static abstract class Human{  
  4.           
  5.     }  
  6.       
  7.     static class Man extends Human{  
  8.           
  9.     }  
  10.       
  11.     static class Woman extends Human{  
  12.           
  13.     }  
  14.   
  15.     public void sayHello(Human human){  
  16.         System.out.println("human say hello");  
  17.     }  
  18.       
  19.     public void sayHello(Man man){  
  20.         System.out.println("man say hello");  
  21.     }  
  22.       
  23.     public void sayHello(Woman woman){  
  24.         System.out.println("woman say hello");  
  25.     }  
  26.       
  27.     /** 
  28.      * @param args 
  29.      */  
  30.     public static void main(String[] args) {  
  31.         Human man = new Man();  
  32.         Human woman = new Woman();  
  33.         StaticDispatch sd = new StaticDispatch();  
  34.         sd.sayHello(man);  
  35.         sd.sayHello(woman);  
  36.     }  
  37.   
  38. }  

 

最後的輸出是

console 寫道
human say hello 
human say hello

 

這個就是很典型的靜態分派.看這段代碼

 

Human man = new Man(); 
Human woman = new Woman();

    

     其中的Human 稱爲變量的靜態類型,而後面的Man稱爲變量的實際類型. 靜態類型是在編譯器可見的,而動態類型必須在運行期才知道.再分析這段調用的方法

  

StaticDispatch sd = new StaticDispatch(); 
sd.sayHello(man); 
sd.sayHello(woman);

 

    我們看到,調用方法的接受者是確定的,都是sd.在靜態分派中,jvm如何確定具體調用哪個目標方法就完全取決於傳入參數的數量和數據類型.而且是根據數據的靜態類型..正因爲如此,這兩個sayHello方法,最後都調用了public void sayHello(Human human);方法.

 

   但是,仔細看會發現,我舉的這個例子,雖然確實是通過靜態分派的,但是具體的方法卻是虛方法..也就是說,

 
虛方法也可能是被靜態分派的.特別注意,重載就是通過靜態分派的

 

   其實非虛方法的靜態分派是完全合理的,後面會再舉一個例子,來確定只要是非虛方法,肯定是通過靜態分派的.

   本節最後的問題是

寫道
Java語言中方法重載採用靜態分派是JVM規範規定的還是語言級別的規定?

 

    這個問題曾經讓我有過困惑.因爲上面這個重載的例子中,

Java代碼  收藏代碼
  1. sd.sayHello(man);   
  2. sd.sayHello(woman);  

    這兩個sayHello方法都是用invokevirtual 指令(關於這個指令,後面會開專門的一節說明)的,那麼其實完全可以採用動態分派,根據man 和 woman 的實際類型來決定調用哪個方法.但是實際上jvm缺沒這麼做.一直等我在仔細看了Java語言"單分派還是多分派"這個內容以後,纔有了答案.下面會專門開一節說這個單分派和多分派.這個問題也在後面解答. 

 

2 動態分派 

    可以說,動態方法分派是Java實現多態的一個重要基礎.因爲,它是Java多態之一----重寫的基礎.看下面的代碼,,摘自 <深入JAVA虛擬機-jvm高級特性與最佳實踐>

Java代碼  收藏代碼
  1. public class DynamicDispatch {  
  2.       
  3.     static abstract class Human{  
  4.         protected abstract void sayHello();  
  5.     }  
  6.       
  7.     static class Man extends Human{  
  8.   
  9.         @Override  
  10.         protected void sayHello() {  
  11.             System.out.println("man say hello");  
  12.         }  
  13.           
  14.     }  
  15.       
  16.     static class Woman extends Human{  
  17.   
  18.         @Override  
  19.         protected void sayHello() {  
  20.             System.out.println("woman say hello");  
  21.         }  
  22.           
  23.     }  
  24.   
  25.     /** 
  26.      * @param args 
  27.      */  
  28.     public static void main(String[] args) {  
  29.         Human man = new Man();  
  30.         Human woman = new Woman();  
  31.         man.sayHello();  
  32.         woman.sayHello();  
  33.     }  
  34.   
  35. }  

 

console 寫道
man say hello 
woman say hello 

 

    只要有一點Java基礎的人基本都能看懂這段代碼.一個非常簡單的重寫.具體看它的結果,很明顯這裏已經不是靜態分派了.因爲man和woman在編譯器都是Human類型,如果是靜態分派,那麼這兩個調用的方法應該是同一個.但是實際上,它們卻調用了對應的真實類型的方法.這就是動態分派.

 

3 invokespecial和invokevirtual指令

    說這個,最主要是由於上面說的那個討論引起的(詳情http://hllvm.group.iteye.com/group/topic/27064).代碼還是放上來吧.

 

   代碼1

Java代碼  收藏代碼
  1. public class SuperTest {  
  2.     public static void main(String[] args) {  
  3.         new Sub().exampleMethod();  
  4.     }  
  5. }  
  6.   
  7. class Super {  
  8.     <span style="color: #ff0000;">private</span> void interestingMethod() {  
  9.         System.out.println("Super's interestingMethod");  
  10.     }  
  11.   
  12.     void exampleMethod() {  
  13.         interestingMethod();  
  14.     }  
  15. }  
  16.   
  17. class Sub extends Super {  
  18.   
  19.     void interestingMethod() {  
  20.         System.out.println("Sub's interestingMethod");  
  21.     }  
  22. }  

 

console輸出
Super's interestingMethod 

 

  代碼2

Java代碼  收藏代碼
  1. public class SuperTest {  
  2.     public static void main(String[] args) {  
  3.         new Sub().exampleMethod();  
  4.     }  
  5. }  
  6.   
  7. class Super {  
  8.     void interestingMethod() {  
  9.         System.out.println("Super's interestingMethod");  
  10.     }  
  11.   
  12.     void exampleMethod() {  
  13.         interestingMethod();  
  14.     }  
  15. }  
  16.   
  17. class Sub extends Super {  
  18.   
  19.     void interestingMethod() {  
  20.         System.out.println("Sub's interestingMethod");  
  21.     }  
  22. }  

 

console輸出 寫道
Sub's interestingMethod

 

    代碼一與代碼二,只有一個區別,就是在代碼一中Super類的interestingMethod方法的修飾符多一個private.根據執行最後的結果來看卻是直接造成了方法分派的不同.一個執行了父類的interestingMethod方法,而一個執行了子類的interestingMethod方法.

    對於這個例子,撒迦的回答比較明確

寫道
關鍵點在於“Java裏什麼是虛方法”以及“虛方法如何分派”。 
Java裏只有非private的成員方法是虛方法。 

所以你會留意到在頂樓例子的第一個版本里,exampleMethod()是用invokespecial來調用interestingMethod()的;而第二個版本里則是用invokevirtual。

    在本文的開頭已經解釋了"什麼是虛方法"這個問題.可以知道,代碼一中Super類的interestingMethod方法是非虛方法(因爲第一個是private方法),而代碼二則是虛方法.可以明確的是

 

寫道
非虛方法肯定是用靜態分派

 

    所以,在代碼一中,使用靜態分派,Super類中的exampleMethod方法調用的是自己類中的interestingMethod方法.這個是編譯器就已經確定的.而代碼二中,exampleMethod方法執行哪個interestingMethod方法就需要看真實對象是哪個.在本例中,真實對象肯定是Sub類.所以就調用Sub類的interestingMethod方法.

   

    上面的這一段分析很簡單,我們可以通過javap輸出看看對應的信息(只需要看Super類的輸出就可以了.代碼一和代碼二的唯一區別就是Super類的interestingMethod方法修飾符)

      

 

代碼一的Super類javap輸出
Compiled from "SuperTest.java" 
class Super { 
Super(); 
Code: 
0: aload_0 
1: invokespecial #1 // Method java/lang/Object."<init>": 
()V 
4: return 

void exampleMethod(); 
Code: 
0: aload_0 
1: ldc #5 // String aa 
3: invokespecial #6 // Method interestingMethod:(Ljava/l 
ang/String;)I 
6: pop 
7: return 
}

 

代碼二的Super類javap輸出 寫道
Compiled from "SuperTest.java" 
class Super { 
Super(); 
Code: 
0: aload_0 
1: invokespecial #1 // Method java/lang/Object."<init>": 
()V 
4: return 

int interestingMethod(java.lang.String); 
Code: 
0: getstatic #2 // Field java/lang/System.out:Ljava/ 
io/PrintStream; 
3: ldc #3 // String Super's interestingMethod 
5: invokevirtual #4 // Method java/io/PrintStream.printl 
n:(Ljava/lang/String;)V 
8: iconst_1 
9: ireturn 

void exampleMethod(); 
Code: 
0: aload_0 
1: ldc #5 // String aa 
3: invokevirtual #6 // Method interestingMethod:(Ljava/l 
ang/String;)I 
6: pop 
7: return 
}

 

    兩邊的不同我通過加粗來說明了.正如撒迦說的,在Super類的exampleMethod方法中調用interestingMethod方法的指令是不同的,代碼一採用的是invokespecial 而代碼二採用的是invokevirtual .

 

寫道
· invokespecial - super方法調用、private方法調用與構造器調用 
· invokevirtual - 用於調用一般實例方法(包括聲明爲final但不爲private的實例方法) 

    

其中

   

寫道
invokespecial調用的目標必然是可以靜態綁定的,因爲它們都無法參與子類型多態;invokevirtual的則一般需要做運行時綁定

 

    到這裏,我們可以明確的是,使用invokespecial 指令的肯定是靜態方法分配的,但是使用invokevirtual卻還不一定()..我們可以看一下本文說靜態分配的那個例子的javap輸出(StaticDispatch類)

 

StaticDispatch類的javap輸出
public class StaticDispatch { 
public StaticDispatch(); 
Code: 
0: aload_0 
1: invokespecial #1 // Method java/lang/Object."<init>": 
()V 
4: return 

public void sayHello(StaticDispatch$Human); 
Code: 
0: getstatic #2 // Field java/lang/System.out:Ljava/ 
io/PrintStream; 
3: ldc #3 // String human say hello 
5: invokevirtual #4 // Method java/io/PrintStream.printl 
n:(Ljava/lang/String;)V 
8: return 

public void sayHello(StaticDispatch$Man); 
Code: 
0: getstatic #2 // Field java/lang/System.out:Ljava/ 
io/PrintStream; 
3: ldc #5 // String man say hello 
5: invokevirtual #4 // Method java/io/PrintStream.printl 
n:(Ljava/lang/String;)V 
8: return 

public void sayHello(StaticDispatch$Woman); 
Code: 
0: getstatic #2 // Field java/lang/System.out:Ljava/ 
io/PrintStream; 
3: ldc #6 // String woman say hello 
5: invokevirtual #4 // Method java/io/PrintStream.printl 
n:(Ljava/lang/String;)V 
8: return 

public static void main(java.lang.String[]); 
Code: 
0: new #7 // class StaticDispatch$Man 
3: dup 
4: invokespecial #8 // Method StaticDispatch$Man."<init> 
":()V 
7: astore_1 
8: new #9 // class StaticDispatch$Woman 
11: dup 
12: invokespecial #10 // Method StaticDispatch$Woman."<ini 
t>":()V 
15: astore_2 
16: new #11 // class StaticDispatch 
19: dup 
20: invokespecial #12 // Method "<init>":()V 
23: astore_3 
24: aload_3 
25: aload_1 
26: invokevirtual #13 // Method sayHello:(LStaticDispatch$ 
Human;)V 
29: aload_3 
30: aload_2 
31: invokevirtual #13 // Method sayHello:(LStaticDispatch$ 
Human;)V 
34: return 
}

 

    由此,我們可以看到,其實invokevirtual  也有可能是靜態分派的.也就是說

寫道
invokevirtual 指令與動態分派沒有直接的聯繫.但是invokespecial調用的目標必然是可以靜態綁定的

  

    本節的最後,必須還要說一下invokevirtual ,invokespecial指令與虛方法之間的關係.雖然invokevirtual 與方法分派沒有直接的關係,但是這兩個指令與虛方法之間還是有非常大的聯繫的.

寫道
所有invokespecial指令調用的方法都是非虛方法,而非虛方法也都是用invokespecial方法調用的.但是,後半句有兩個例外,static修飾的方法與final修飾且非private的方法
虛方法都是通過invokevirtual指令來調用的

    

     上面說的兩個例外說明一下, static修飾的方法通過invokestatic 指令來調用.

     而final修飾且非private的方法也是用invokevirtual指令來調用的.這個可以看下撒迦的說明

RednaxelaFX 寫道
直接把答案說出來就不有趣了。讓我舉個例子來誘導一下。 
關鍵詞:分離編譯,二進制兼容性 

A.java 
Java代碼  收藏代碼
  1. public class A {  
  2.   public void foo() { /* ... */ }  
  3. }  


B.java 
Java代碼  收藏代碼
  1. public class B extends A {  
  2.   public void foo() { /* ... */ }  
  3. }  


C.java 
Java代碼  收藏代碼
  1. public class C extends B {  
  2.   public final void foo() { /* ... */ }  
  3. }  


這樣的話有3個源碼文件,它們可以分別編譯。三個類有繼承關係,每個都有自己的foo()的實現。其中C.foo()是final的。 

那麼如果在別的什麼地方, 
Java代碼  收藏代碼
  1. A a = getA();  
  2. a.foo();  

這個a.foo()應該使用invokevirtual是很直觀的對吧? 
而這個實際的調用目標也有可能是C.foo(),對吧? 

所以爲了設計的簡單性,以及更好的二進制兼容性……(此處省略

 4 單分派與多分派

    首先解釋一下這兩個概念.在《Java與模式》中的譯文中提出了宗量這個概念。

 

寫道
方法的接受者與方法的參數統稱爲方法的宗量。根據分派基於多少種宗量,可以將分派劃分爲單分派和多分派。

     “方法的接受者”這個本文上面已經有說明了,而“方法的參數”就是指方法的參數類型和個數。 

    其實這個定義並不好理解。我找不到其他好的例子來說明這個,所以採用《深入Java虛擬機--JVM高級特性與最佳實踐》的例子說明,包括後面的說明很多都來自此書

 

Java代碼  收藏代碼
  1. public class Dispatcher {  
  2.   
  3.     static class QQ {  
  4.     }  
  5.   
  6.     static class _360 {  
  7.     }  
  8.   
  9.     public static class Father {  
  10.         public void hardChoice(QQ qq) {  
  11.             System.out.println("father choose qq");  
  12.         }  
  13.   
  14.         public void hardChoice(_360 _360) {  
  15.             System.out.println("father choose 360");  
  16.         }  
  17.     }  
  18.       
  19.     public static class Son extends Father{  
  20.         public void hardChoice(QQ qq) {  
  21.             System.out.println("son choose qq");  
  22.         }  
  23.   
  24.         public void hardChoice(_360 _360) {  
  25.             System.out.println("son choose 360");  
  26.         }  
  27.     }  
  28.   
  29.     public static void main(String[] args) {  
  30.         Father father = new Father();  
  31.         Father son = new Son();  
  32.         father.hardChoice(new _360());  
  33.         son.hardChoice(new QQ());  
  34.           
  35.     }  
  36.   
  37. }  

 

     

console輸出 寫道
father choose 360 
son choose qq 

 

    上面的例子中 ,我們需要關心的主要是這兩行代碼

Java代碼  收藏代碼
  1. father.hardChoice(new _360());  
  2. son.hardChoice(new QQ());  

     我們分別從編譯階段和運行階段分別分析這個分派的過程。在編譯階段,jvm在選擇哪個hardChoice方法的時候有兩點依據:一是靜態類型是Fatcher還是Son.二是方法參數的QQ還是360。根據這兩點,在靜態編譯的時候,這兩行代碼會被翻譯成 Father.hardChoice(360)和 Father.hardChoice(QQ).到這裏,我們就可以知道,

 

寫道
Java是靜態多分派的語言

 

    在運行階段,執行 son.hardChoice(new QQ()); 的時候,由於編譯器已經在編譯階段決定目標方法的簽名必須是 “hardChoice(QQ)”,jvm此時不會關心傳遞過來的QQ參數到底是 “騰訊QQ”還是“奇瑞QQ”,因爲這個時候參數的靜態類型,實際類型都不會對方法的分派構成任何影響,唯一可以影響jvm進行方法分派的只有該方法的接受者,也就是son。這個時候,其實就是一個宗量作爲分派的選擇,也就是

 

寫道
Java是動態單分派的語言

 

   我想應該很多人對靜態多分派的說明不會有疑義,而對動態單分派會有一些疑問。因爲我第一次看的時候也覺得,就上面這個QQ和360的例子並不能十分好的解釋在運行期動態分派的時候,jvm只對方法的接受者敏感,而對方法的參數無視。我想大家是否有想到我在本文第一節說靜態分派的時候提到的那個問題:

寫道
Java語言中方法重載採用靜態分派是JVM規範規定的還是語言級別的規定? 

    在靜態分派的那個重載的例子中:

Java代碼  收藏代碼
  1. Human man = new Man();  
  2. Human woman = new Woman();  
  3. StaticDispatch sd = new StaticDispatch();  
  4. sd.sayHello(man);  
  5. sd.sayHello(woman);   
console輸出 寫道
human say hello 
human say hello

    可以想想,爲什麼最後都會執行 Human類的sayHello方法。這裏就可以有很明確的解釋了,就是因爲Java語言是動態單分派的!在編譯階段 man和woman都是Human類型,所以在運行時調用sd.sayHello(man);和 sd.sayHello(woman);的時候,jvm已經不關心sayHello方法參數的真實類型是什麼了,它只關心具體的接受者是什麼。那麼,結果顯而易見,他們都會調用Human類的sayHello方法。所以,

   

寫道
Java語言對重載採用靜態分派的原因在於Java是動態單分派的!

 

    最後,摘錄下《深入Java虛擬機--JVM高級特性與最佳實踐》中關於動態分派的說明:

寫道
今天(JDK1.6時期)的Java語言是一門靜態多分派,動態多分派的語言。強調“今天的Java語言”是因爲這個結論未必會恆久不變,C#在3.0以及之前的版本與Java醫院也是動態單分派的語言,但是在C#4.0中引入dynamic類型以後,就可以方便地實現動態多分派。Java也已經在JSR-292中開始規劃對動態語言的支持了,日後很可能提供類似的動態類型功能。

 

  

最後,再次感謝撒迦與《深入Java虛擬機--JVM高級特性與最佳實踐》對本文的大力支持。哈哈 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章