說明:這兩天遇到的一些Java方法分派的問題,結合自己書上看的,google的,還有撒迦教我的,做一個總結吧.望指正.
很多的內容可以參加撒迦的這篇博文 : http://rednaxelafx.iteye.com/blog/652719
我這篇裏很多概念的解釋都摘自上面的博文,所以,我就不一一指出啦.在此感謝撒迦的幫助.
還有一些講解(包括代碼)來自 <深入JAVA虛擬機-jvm高級特性與最佳實踐>,很好一本書,推薦對jvm有興趣的同學購買
另外 http://hllvm.group.iteye.com/group/topic/27064 這個帖子也可能幫助大家對方法分派有所瞭解.
1 靜態分派
對於這個靜態分派,撒迦很喜歡叫做非虛方法分派(當然按照他自己的說法就是:我不喜歡叫靜態分派).
首先,解釋一下什麼叫虛方法.虛方法的概念有點難說,不過把什麼是非虛方法說明一下,其他的就是虛方法啦.哈哈
由於非虛方法不能被override,所以自然也不會產生子類複寫的多態效果.這樣的話,方法被調用的入口只可能是一個.而且編譯器可知.也就是說,jvm需要執行哪個方法是在編譯器就已經確定.且在運行期不會變化.很具體的例子就是方法的重載.
看如下例子,摘自 <深入JAVA虛擬機-jvm高級特性與最佳實踐>
- public class StaticDispatch {
- static abstract class Human{
- }
- static class Man extends Human{
- }
- static class Woman extends Human{
- }
- public void sayHello(Human human){
- System.out.println("human say hello");
- }
- public void sayHello(Man man){
- System.out.println("man say hello");
- }
- public void sayHello(Woman woman){
- System.out.println("woman say hello");
- }
- /**
- * @param args
- */
- public static void main(String[] args) {
- Human man = new Man();
- Human woman = new Woman();
- StaticDispatch sd = new StaticDispatch();
- sd.sayHello(man);
- sd.sayHello(woman);
- }
- }
最後的輸出是
human say hello
這個就是很典型的靜態分派.看這段代碼
Human woman = new Woman();
其中的Human 稱爲變量的靜態類型,而後面的Man稱爲變量的實際類型. 靜態類型是在編譯器可見的,而動態類型必須在運行期才知道.再分析這段調用的方法
sd.sayHello(man);
sd.sayHello(woman);
我們看到,調用方法的接受者是確定的,都是sd.在靜態分派中,jvm如何確定具體調用哪個目標方法就完全取決於傳入參數的數量和數據類型.而且是根據數據的靜態類型..正因爲如此,這兩個sayHello方法,最後都調用了public void sayHello(Human human);方法.
但是,仔細看會發現,我舉的這個例子,雖然確實是通過靜態分派的,但是具體的方法卻是虛方法..也就是說,
其實非虛方法的靜態分派是完全合理的,後面會再舉一個例子,來確定只要是非虛方法,肯定是通過靜態分派的.
本節最後的問題是
這個問題曾經讓我有過困惑.因爲上面這個重載的例子中,
- sd.sayHello(man);
- sd.sayHello(woman);
這兩個sayHello方法都是用invokevirtual 指令(關於這個指令,後面會開專門的一節說明)的,那麼其實完全可以採用動態分派,根據man 和 woman 的實際類型來決定調用哪個方法.但是實際上jvm缺沒這麼做.一直等我在仔細看了Java語言"單分派還是多分派"這個內容以後,纔有了答案.下面會專門開一節說這個單分派和多分派.這個問題也在後面解答.
2 動態分派
可以說,動態方法分派是Java實現多態的一個重要基礎.因爲,它是Java多態之一----重寫的基礎.看下面的代碼,,摘自 <深入JAVA虛擬機-jvm高級特性與最佳實踐>
- public class DynamicDispatch {
- 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");
- }
- }
- /**
- * @param args
- */
- public static void main(String[] args) {
- Human man = new Man();
- Human woman = new Woman();
- man.sayHello();
- woman.sayHello();
- }
- }
woman say hello
只要有一點Java基礎的人基本都能看懂這段代碼.一個非常簡單的重寫.具體看它的結果,很明顯這裏已經不是靜態分派了.因爲man和woman在編譯器都是Human類型,如果是靜態分派,那麼這兩個調用的方法應該是同一個.但是實際上,它們卻調用了對應的真實類型的方法.這就是動態分派.
3 invokespecial和invokevirtual指令
說這個,最主要是由於上面說的那個討論引起的(詳情http://hllvm.group.iteye.com/group/topic/27064).代碼還是放上來吧.
代碼1
- public class SuperTest {
- public static void main(String[] args) {
- new Sub().exampleMethod();
- }
- }
- class Super {
- <span style="color: #ff0000;">private</span> void interestingMethod() {
- System.out.println("Super's interestingMethod");
- }
- void exampleMethod() {
- interestingMethod();
- }
- }
- class Sub extends Super {
- void interestingMethod() {
- System.out.println("Sub's interestingMethod");
- }
- }
代碼2
- public class SuperTest {
- public static void main(String[] args) {
- new Sub().exampleMethod();
- }
- }
- class Super {
- void interestingMethod() {
- System.out.println("Super's interestingMethod");
- }
- void exampleMethod() {
- interestingMethod();
- }
- }
- class Sub extends Super {
- void interestingMethod() {
- System.out.println("Sub's interestingMethod");
- }
- }
代碼一與代碼二,只有一個區別,就是在代碼一中Super類的interestingMethod方法的修飾符多一個private.根據執行最後的結果來看卻是直接造成了方法分派的不同.一個執行了父類的interestingMethod方法,而一個執行了子類的interestingMethod方法.
對於這個例子,撒迦的回答比較明確
Java裏只有非private的成員方法是虛方法。
所以你會留意到在頂樓例子的第一個版本里,exampleMethod()是用invokespecial來調用interestingMethod()的;而第二個版本里則是用invokevirtual。
在本文的開頭已經解釋了"什麼是虛方法"這個問題.可以知道,代碼一中Super類的interestingMethod方法是非虛方法(因爲第一個是private方法),而代碼二則是虛方法.可以明確的是
所以,在代碼一中,使用靜態分派,Super類中的exampleMethod方法調用的是自己類中的interestingMethod方法.這個是編譯器就已經確定的.而代碼二中,exampleMethod方法執行哪個interestingMethod方法就需要看真實對象是哪個.在本例中,真實對象肯定是Sub類.所以就調用Sub類的interestingMethod方法.
上面的這一段分析很簡單,我們可以通過javap輸出看看對應的信息(只需要看Super類的輸出就可以了.代碼一和代碼二的唯一區別就是Super類的interestingMethod方法修飾符)
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
}
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 .
· invokevirtual - 用於調用一般實例方法(包括聲明爲final但不爲private的實例方法)
其中
到這裏,我們可以明確的是,使用invokespecial 指令的肯定是靜態方法分配的,但是使用invokevirtual卻還不一定()..我們可以看一下本文說靜態分配的那個例子的javap輸出(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 與方法分派沒有直接的關係,但是這兩個指令與虛方法之間還是有非常大的聯繫的.
上面說的兩個例外說明一下, static修飾的方法通過invokestatic 指令來調用.
而final修飾且非private的方法也是用invokevirtual指令來調用的.這個可以看下撒迦的說明
關鍵詞:分離編譯,二進制兼容性
A.java
- public class A {
- public void foo() { /* ... */ }
- }
B.java
- public class B extends A {
- public void foo() { /* ... */ }
- }
C.java
- public class C extends B {
- public final void foo() { /* ... */ }
- }
這樣的話有3個源碼文件,它們可以分別編譯。三個類有繼承關係,每個都有自己的foo()的實現。其中C.foo()是final的。
那麼如果在別的什麼地方,
- A a = getA();
- a.foo();
這個a.foo()應該使用invokevirtual是很直觀的對吧?
而這個實際的調用目標也有可能是C.foo(),對吧?
所以爲了設計的簡單性,以及更好的二進制兼容性……(此處省略
4 單分派與多分派
首先解釋一下這兩個概念.在《Java與模式》中的譯文中提出了宗量這個概念。
“方法的接受者”這個本文上面已經有說明了,而“方法的參數”就是指方法的參數類型和個數。
其實這個定義並不好理解。我找不到其他好的例子來說明這個,所以採用《深入Java虛擬機--JVM高級特性與最佳實踐》的例子說明,包括後面的說明很多都來自此書
- public class Dispatcher {
- static class QQ {
- }
- static class _360 {
- }
- public static class Father {
- public void hardChoice(QQ qq) {
- System.out.println("father choose qq");
- }
- public void hardChoice(_360 _360) {
- System.out.println("father choose 360");
- }
- }
- public static class Son extends Father{
- public void hardChoice(QQ qq) {
- System.out.println("son choose qq");
- }
- public void hardChoice(_360 _360) {
- System.out.println("son choose 360");
- }
- }
- public static void main(String[] args) {
- Father father = new Father();
- Father son = new Son();
- father.hardChoice(new _360());
- son.hardChoice(new QQ());
- }
- }
son choose qq
上面的例子中 ,我們需要關心的主要是這兩行代碼
- father.hardChoice(new _360());
- son.hardChoice(new QQ());
我們分別從編譯階段和運行階段分別分析這個分派的過程。在編譯階段,jvm在選擇哪個hardChoice方法的時候有兩點依據:一是靜態類型是Fatcher還是Son.二是方法參數的QQ還是360。根據這兩點,在靜態編譯的時候,這兩行代碼會被翻譯成 Father.hardChoice(360)和 Father.hardChoice(QQ).到這裏,我們就可以知道,
在運行階段,執行 son.hardChoice(new QQ()); 的時候,由於編譯器已經在編譯階段決定目標方法的簽名必須是 “hardChoice(QQ)”,jvm此時不會關心傳遞過來的QQ參數到底是 “騰訊QQ”還是“奇瑞QQ”,因爲這個時候參數的靜態類型,實際類型都不會對方法的分派構成任何影響,唯一可以影響jvm進行方法分派的只有該方法的接受者,也就是son。這個時候,其實就是一個宗量作爲分派的選擇,也就是
我想應該很多人對靜態多分派的說明不會有疑義,而對動態單分派會有一些疑問。因爲我第一次看的時候也覺得,就上面這個QQ和360的例子並不能十分好的解釋在運行期動態分派的時候,jvm只對方法的接受者敏感,而對方法的參數無視。我想大家是否有想到我在本文第一節說靜態分派的時候提到的那個問題:
在靜態分派的那個重載的例子中:
- Human man = new Man();
- Human woman = new Woman();
- StaticDispatch sd = new StaticDispatch();
- sd.sayHello(man);
- sd.sayHello(woman);
human say hello
可以想想,爲什麼最後都會執行 Human類的sayHello方法。這裏就可以有很明確的解釋了,就是因爲Java語言是動態單分派的!在編譯階段 man和woman都是Human類型,所以在運行時調用sd.sayHello(man);和 sd.sayHello(woman);的時候,jvm已經不關心sayHello方法參數的真實類型是什麼了,它只關心具體的接受者是什麼。那麼,結果顯而易見,他們都會調用Human類的sayHello方法。所以,
最後,摘錄下《深入Java虛擬機--JVM高級特性與最佳實踐》中關於動態分派的說明:
最後,再次感謝撒迦與《深入Java虛擬機--JVM高級特性與最佳實踐》對本文的大力支持。哈哈