JVM之方法调用-分派
說明:這兩天遇到的一些Java方法分派的問題,結(jié)合自己書上看的,google的,還有撒迦教我的,做一個總結(jié)吧.望指正.
?
寫道 方法分派指的是虛擬機如何確定應(yīng)該執(zhí)行哪個方法!?
很多的內(nèi)容可以參加撒迦的這篇博文 :?http://rednaxelafx.iteye.com/blog/652719
我這篇里很多概念的解釋都摘自上面的博文,所以,我就不一一指出啦.在此感謝撒迦的幫助.
還有一些講解(包括代碼)來自 <深入JAVA虛擬機-jvm高級特性與最佳實踐>,很好一本書,推薦對jvm有興趣的同學(xué)購買
另外?http://hllvm.group.iteye.com/group/topic/27064?這個帖子也可能幫助大家對方法分派有所了解.
1 靜態(tài)分派
??? 對于這個靜態(tài)分派,撒迦很喜歡叫做非虛方法分派(當然按照他自己的說法就是:我不喜歡叫靜態(tài)分派).
??? 首先,解釋一下什么叫虛方法.虛方法的概念有點難說,不過把什么是非虛方法說明一下,其他的就是虛方法啦.哈哈
非虛方法是所有的類方法(也就是申明為static的方法) + 所有聲明為final或private的實例方法.?
??? 由于非虛方法不能被override,所以自然也不會產(chǎn)生子類復(fù)寫的多態(tài)效果.這樣的話,方法被調(diào)用的入口只可能是一個.而且編譯器可知.也就是說,jvm需要執(zhí)行哪個方法是在編譯器就已經(jīng)確定.且在運行期不會變化.很具體的例子就是方法的重載.
??? 看如下例子,摘自 <深入JAVA虛擬機-jvm高級特性與最佳實踐>
?
Java代碼???
最后的輸出是
console 寫道 human say hello?human say hello
?
這個就是很典型的靜態(tài)分派.看這段代碼
?
Human man = new Man();?Human woman = new Woman();
????
???? 其中的Human 稱為變量的靜態(tài)類型,而后面的Man稱為變量的實際類型. 靜態(tài)類型是在編譯器可見的,而動態(tài)類型必須在運行期才知道.再分析這段調(diào)用的方法
??
StaticDispatch sd = new StaticDispatch();?sd.sayHello(man);?
sd.sayHello(woman);
?
??? 我們看到,調(diào)用方法的接受者是確定的,都是sd.在靜態(tài)分派中,jvm如何確定具體調(diào)用哪個目標方法就完全取決于傳入?yún)?shù)的數(shù)量和數(shù)據(jù)類型.而且是根據(jù)數(shù)據(jù)的靜態(tài)類型..正因為如此,這兩個sayHello方法,最后都調(diào)用了public void sayHello(Human human);方法.
?
?? 但是,仔細看會發(fā)現(xiàn),我舉的這個例子,雖然確實是通過靜態(tài)分派的,但是具體的方法卻是虛方法..也就是說,
虛方法也可能是被靜態(tài)分派的.特別注意,重載就是通過靜態(tài)分派的?
?? 其實非虛方法的靜態(tài)分派是完全合理的,后面會再舉一個例子,來確定只要是非虛方法,肯定是通過靜態(tài)分派的.
?? 本節(jié)最后的問題是
寫道 Java語言中方法重載采用靜態(tài)分派是JVM規(guī)范規(guī)定的還是語言級別的規(guī)定??
??? 這個問題曾經(jīng)讓我有過困惑.因為上面這個重載的例子中,
Java代碼????? 這兩個sayHello方法都是用invokevirtual 指令(關(guān)于這個指令,后面會開專門的一節(jié)說明)的,那么其實完全可以采用動態(tài)分派,根據(jù)man 和 woman 的實際類型來決定調(diào)用哪個方法.但是實際上jvm缺沒這么做.一直等我在仔細看了Java語言"單分派還是多分派"這個內(nèi)容以后,才有了答案.下面會專門開一節(jié)說這個單分派和多分派.這個問題也在后面解答.?
?
2 動態(tài)分派?
??? 可以說,動態(tài)方法分派是Java實現(xiàn)多態(tài)的一個重要基礎(chǔ).因為,它是Java多態(tài)之一----重寫的基礎(chǔ).看下面的代碼,,摘自 <深入JAVA虛擬機-jvm高級特性與最佳實踐>
Java代碼???
console 寫道 man say hello?woman say hello?
?
??? 只要有一點Java基礎(chǔ)的人基本都能看懂這段代碼.一個非常簡單的重寫.具體看它的結(jié)果,很明顯這里已經(jīng)不是靜態(tài)分派了.因為man和woman在編譯器都是Human類型,如果是靜態(tài)分派,那么這兩個調(diào)用的方法應(yīng)該是同一個.但是實際上,它們卻調(diào)用了對應(yīng)的真實類型的方法.這就是動態(tài)分派.
?
3 invokespecial和invokevirtual指令
??? 說這個,最主要是由于上面說的那個討論引起的(詳情http://hllvm.group.iteye.com/group/topic/27064).代碼還是放上來吧.
?
???代碼1
Java代碼???
console輸出 Super's?interestingMethod??
??代碼2
Java代碼???
console輸出 寫道 Sub's?interestingMethod?
??? 代碼一與代碼二,只有一個區(qū)別,就是在代碼一中Super類的interestingMethod方法的修飾符多一個private.根據(jù)執(zhí)行最后的結(jié)果來看卻是直接造成了方法分派的不同.一個執(zhí)行了父類的interestingMethod方法,而一個執(zhí)行了子類的interestingMethod方法.
??? 對于這個例子,撒迦的回答比較明確
寫道 關(guān)鍵點在于“Java里什么是虛方法”以及“虛方法如何分派”。?Java里只有非private的成員方法是虛方法。?
所以你會留意到在頂樓例子的第一個版本里,exampleMethod()是用invokespecial來調(diào)用interestingMethod()的;而第二個版本里則是用invokevirtual。
??? 在本文的開頭已經(jīng)解釋了"什么是虛方法"這個問題.可以知道,代碼一中Super類的interestingMethod方法是非虛方法(因為第一個是private方法),而代碼二則是虛方法.可以明確的是
?
寫道 非虛方法肯定是用靜態(tài)分派?
??? 所以,在代碼一中,使用靜態(tài)分派,Super類中的exampleMethod方法調(diào)用的是自己類中的interestingMethod方法.這個是編譯器就已經(jīng)確定的.而代碼二中,exampleMethod方法執(zhí)行哪個interestingMethod方法就需要看真實對象是哪個.在本例中,真實對象肯定是Sub類.所以就調(diào)用Sub類的interestingMethod方法.
???
??? 上面的這一段分析很簡單,我們可以通過javap輸出看看對應(yīng)的信息(只需要看Super類的輸出就可以了.代碼一和代碼二的唯一區(qū)別就是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方法中調(diào)用interestingMethod方法的指令是不同的,代碼一采用的是invokespecial?而代碼二采用的是invokevirtual .
?
寫道 · invokespecial - super方法調(diào)用、private方法調(diào)用與構(gòu)造器調(diào)用?· invokevirtual - 用于調(diào)用一般實例方法(包括聲明為final但不為private的實例方法)?
????
其中
???
寫道 invokespecial調(diào)用的目標必然是可以靜態(tài)綁定的,因為它們都無法參與子類型多態(tài);invokevirtual的則一般需要做運行時綁定?
??? 到這里,我們可以明確的是,使用invokespecial 指令的肯定是靜態(tài)方法分配的,但是使用invokevirtual卻還不一定()..我們可以看一下本文說靜態(tài)分配的那個例子的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? 也有可能是靜態(tài)分派的.也就是說
寫道 invokevirtual 指令與動態(tài)分派沒有直接的聯(lián)系.但是invokespecial調(diào)用的目標必然是可以靜態(tài)綁定的??
??? 本節(jié)的最后,必須還要說一下invokevirtual ,invokespecial指令與虛方法之間的關(guān)系.雖然invokevirtual 與方法分派沒有直接的關(guān)系,但是這兩個指令與虛方法之間還是有非常大的聯(lián)系的.
寫道 所有invokespecial指令調(diào)用的方法都是非虛方法,而非虛方法也都是用invokespecial方法調(diào)用的.但是,后半句有兩個例外,static修飾的方法與final修飾且非private的方法 虛方法都是通過invokevirtual指令來調(diào)用的????
??? ?上面說的兩個例外說明一下, static修飾的方法通過invokestatic 指令來調(diào)用.
???? 而final修飾且非private的方法也是用invokevirtual指令來調(diào)用的.這個可以看下撒迦的說明
RednaxelaFX 寫道 直接把答案說出來就不有趣了。讓我舉個例子來誘導(dǎo)一下。?關(guān)鍵詞:分離編譯,二進制兼容性?
A.java?
Java代碼??
B.java?
Java代碼??
C.java?
Java代碼??
這樣的話有3個源碼文件,它們可以分別編譯。三個類有繼承關(guān)系,每個都有自己的foo()的實現(xiàn)。其中C.foo()是final的。?
那么如果在別的什么地方,?
Java代碼??
這個a.foo()應(yīng)該使用invokevirtual是很直觀的對吧??
而這個實際的調(diào)用目標也有可能是C.foo(),對吧??
所以為了設(shè)計的簡單性,以及更好的二進制兼容性……(此處省略
?4 單分派與多分派
??? 首先解釋一下這兩個概念.在《Java與模式》中的譯文中提出了宗量這個概念。
?
寫道 方法的接受者與方法的參數(shù)統(tǒng)稱為方法的宗量。根據(jù)分派基于多少種宗量,可以將分派劃分為單分派和多分派。???? “方法的接受者”這個本文上面已經(jīng)有說明了,而“方法的參數(shù)”就是指方法的參數(shù)類型和個數(shù)。?
??? 其實這個定義并不好理解。我找不到其他好的例子來說明這個,所以采用《深入Java虛擬機--JVM高級特性與最佳實踐》的例子說明,包括后面的說明很多都來自此書
?
Java代碼???
?????
console輸出 寫道 father choose 360?son choose qq?
?
??? 上面的例子中 ,我們需要關(guān)心的主要是這兩行代碼
Java代碼?????? 我們分別從編譯階段和運行階段分別分析這個分派的過程。在編譯階段,jvm在選擇哪個hardChoice方法的時候有兩點依據(jù):一是靜態(tài)類型是Fatcher還是Son.二是方法參數(shù)的QQ還是360。根據(jù)這兩點,在靜態(tài)編譯的時候,這兩行代碼會被翻譯成 Father.hardChoice(360)和 Father.hardChoice(QQ).到這里,我們就可以知道,
?
寫道 Java是靜態(tài)多分派的語言?
??? 在運行階段,執(zhí)行 son.hardChoice(new QQ());?的時候,由于編譯器已經(jīng)在編譯階段決定目標方法的簽名必須是 “hardChoice(QQ)”,jvm此時不會關(guān)心傳遞過來的QQ參數(shù)到底是 “騰訊QQ”還是“奇瑞QQ”,因為這個時候參數(shù)的靜態(tài)類型,實際類型都不會對方法的分派構(gòu)成任何影響,唯一可以影響jvm進行方法分派的只有該方法的接受者,也就是son。這個時候,其實就是一個宗量作為分派的選擇,也就是
?
寫道 Java是動態(tài)單分派的語言?
?? 我想應(yīng)該很多人對靜態(tài)多分派的說明不會有疑義,而對動態(tài)單分派會有一些疑問。因為我第一次看的時候也覺得,就上面這個QQ和360的例子并不能十分好的解釋在運行期動態(tài)分派的時候,jvm只對方法的接受者敏感,而對方法的參數(shù)無視。我想大家是否有想到我在本文第一節(jié)說靜態(tài)分派的時候提到的那個問題:
寫道 Java語言中方法重載采用靜態(tài)分派是JVM規(guī)范規(guī)定的還是語言級別的規(guī)定????? 在靜態(tài)分派的那個重載的例子中:
Java代碼??human say hello
??? 可以想想,為什么最后都會執(zhí)行 Human類的sayHello方法。這里就可以有很明確的解釋了,就是因為Java語言是動態(tài)單分派的!在編譯階段 man和woman都是Human類型,所以在運行時調(diào)用sd.sayHello(man);和 sd.sayHello(woman);的時候,jvm已經(jīng)不關(guān)心sayHello方法參數(shù)的真實類型是什么了,它只關(guān)心具體的接受者是什么。那么,結(jié)果顯而易見,他們都會調(diào)用Human類的sayHello方法。所以,
???
寫道 Java語言對重載采用靜態(tài)分派的原因在于Java是動態(tài)單分派的!?
??? 最后,摘錄下《深入Java虛擬機--JVM高級特性與最佳實踐》中關(guān)于動態(tài)分派的說明:
寫道 今天(JDK1.6時期)的Java語言是一門靜態(tài)多分派,動態(tài)多分派的語言。強調(diào)“今天的Java語言”是因為這個結(jié)論未必會恒久不變,C#在3.0以及之前的版本與Java醫(yī)院也是動態(tài)單分派的語言,但是在C#4.0中引入dynamic類型以后,就可以方便地實現(xiàn)動態(tài)多分派。Java也已經(jīng)在JSR-292中開始規(guī)劃對動態(tài)語言的支持了,日后很可能提供類似的動態(tài)類型功能。?
??
最后,再次感謝撒迦與《深入Java虛擬機--JVM高級特性與最佳實踐》對本文的大力支持。哈哈?
總結(jié)
以上是生活随笔為你收集整理的JVM之方法调用-分派的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: RTP音频流分析以及乱序问题的解决方法(
- 下一篇: 阿里P10毕玄:Java大牛程序员的学习