某次逛論壇時發現一個非常有意思的題目,如下:
class A<B>
{
public String show(A obj)
{
return ("A and A");
}
public String show(B obj)
{
return ("A and B");
}
}
class B extends A
{
public String show(B obj)
{
return ("B and B");
}
public String show(A obj)
{
return ("B and A");
}
}
A a = new B();
B b = new B();
System.out.println(a.show(b));
上面的代碼正確的結果會輸出B and A,剛開始看到的時候也感覺莫名其妙,實際上這裏包含有不少知識點,稍微不留神就會弄錯。題目本身輸出的結果不重要,關鍵是我們要掌握裏面的知識點,明白爲什麼會這樣輸出,最犀利的方式還是從字節碼的角度來觀察。同樣javap命令輸出上面代碼的字節碼。
Compiled from "Test.java"
class A extends java.lang.Object{
A();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public java.lang.String show(A);
Code:
0: ldc #2; //String A and A
2: areturn
public java.lang.String show(java.lang.Object);
Code:
0: ldc #3; //String A and B
2: areturn
Compiled from "Test.java"
class B extends A{
B();
Code:
0: aload_0
1: invokespecial #1; //Method A."<init>":()V
4: return
public java.lang.String show(B);
Code:
0: ldc #2; //String B and B
2: areturn
public java.lang.String show(A);
Code:
0: ldc #3; //String B and A
2: areturn
}
public static void main(java.lang.String[]);
Code:
0: new #2; //class B
3: dup
4: invokespecial #3; //Method B."<init>":()V
7: astore_1
8: new #2; //class B
11: dup
12: invokespecial #3; //Method B."<init>":()V
15: astore_2
16: getstatic #4; //Field java/lang/System.out:Ljava/io/PrintStream;
19: aload_1
20: aload_2
21: invokevirtual #5; //Method A.show:(LA;)Ljava/lang/String;
24: invokevirtual #6; //Method java/io/PrintStream.println:(Ljava/lang/Str
ing;)V
27: return
}
上面是類A,類B和main方法的字節碼,先來看下類A的字節碼,可以很神奇的發現show(B obj)這個方法不見了,其實代碼爲了增加複雜性,故意將該方法寫成show(B obj),實際上類A是一個泛型類,這裏用符號B來表示的,可以換成任意的其他符號。衆所周知的是java中泛型是僞泛型,在編譯期會發生一個叫做類型擦除的動作,關於泛型的類型擦除有很多可以講的東西,建議讀者自行去百度一下。因此這裏發生類型擦出後,實際存在的方法爲show(Object obj)。這裏是非常關鍵的一點。
類B的字節碼沒有什麼特殊的內容,只是定義了兩個方法show(B),show(A),不過要注意的是會覆寫類A中的show(A)方法,同時繼承show(Object obj),因此類B中有三個方法。
然後再來看下main方法的字節碼,一步一步的過:
new指令在堆中分配類B需要的內存並初始化成員變量爲默認值,返回執行該地址的指針壓棧。
dup指令複製當前棧頂的元素
invokespecial調用B的初始化函數,消耗一個棧頂元素
astore_1將棧頂元素彈出賦值給局部變量表的第二個變量這裏是A a
然後後面類似的操作
astore_2將棧頂元素彈出賦值給局部變量表的第三個變量這裏是B b
後面三條指令連續三個壓棧操作先壓入out變量,然後是a,最後是b
invokevirtual方法很關鍵,這裏雖然寫的是A.show(A),但是不得不先提及兩個概念
概念一:靜態綁定
靜態綁定指的是在編譯期間就已經確定了要調用的方法,private、static和final修飾的方法都是靜態綁定的,注意在java中只有方法纔有綁定的概念。
概念二:動態綁定
動態綁定指的是在運行時根據對象實際的類型去尋找要調用的方法。JAVA 虛擬機調用一個類方法時(靜態方法),它會基於對象引用的類型(通常在編譯時可知)來選擇所調用的方法。相反,當虛擬機調用一個實例方法時,它會基於對象實際的類型(只能在運行時得知)來選擇所調用的方法,這就是動態綁定,是多態的一種。
介紹完這兩個概念接着看invokevirtual指令,由於引用的類型爲A,因此會首先搜索A的方法表信息,發現show(A)方法最符合,所以這裏編譯的時候綁定到A.show(A),但是在運行中會發生動態綁定,當發現實際對象類型爲B時,會在B的方法表中尋找最合適的方法,如果沒找到則向上尋找父類中合適的方法,這裏由於B覆寫了父類的show(A)方法,因此會調用B的show(A)方法。
以上就是一道題目引出的知識點,包括字節碼的解釋,靜態動態綁定,泛型擦除。