前一段時間看了《深入理解JVM》第三部分虛擬機執行子系統的內容,看到了重載與重寫在JVM層面的調用原理(詳見8.3 方法調用一節),但是沒有寫成博客總結一下,這裡討論討論。在討論過程中,難免會涉及到 字節碼指令 相關的內容。
1.重載(overload)方法
對重載方法的調用主要看靜態類型,靜態類型是什麼類型,就調用什麼類型的參數方法。
2.重寫(override)方法
對重寫方法的調用主要看實際類型。實際類型如果實現了該方法則直接調用該方法,如果沒有實現,則在繼承關系中從低到高搜索有無實現。
3.
java文件的編譯過程中不存在傳統編譯的連接過程,一切方法調用在class文件中存放的只是符號引用,而不是方法在實際運行時內存布局中的入口地址。
1.靜態類型與實際類型,方法接收者
Human man = new Man();
man.foo();
上面這條語句中,man的靜態類型為Human,實際類型為Man。所謂方法接收者,就是指將要執行foo()方法的所有者(在多態中,有可能是父類Human的對象,也可能是子類Man的對象)。
2.字節碼的方法調用指令
(1)invokestatic:調用靜態方法
(2)invokespecial:調用實例構造器方法,私有方法和父類方法。
(3)invokevirtual:調用所有的虛方法。
(4)invokeinterface:調用接口方法,會在運行時再確定一個實現此接口的對象。
(5)invokedynamic:先在運行時動態解析出調用點限定符所引用的方法,然後再執行該方法。
前2條指令(invokestatic, invokespecial),在類加載時就能把符號引用解析為直接引用,符合這個條件的有靜態方法、實例構造器方法、私有方法、父類方法這4類,這4類方法叫非虛方法。
非虛方法除了上面靜態方法、實例構造器方法、私有方法、父類方法這4種方法之外,還包括final方法。雖然final方法使用invokevirtual指令來調用,但是final方法無法被覆蓋,沒有其他版本,無需對方法接收者進行多態選擇,或者說多態選擇的結果是唯一的。
上面說的靜態類型和動態類型都是可以變化的。靜態類型發生變化(強制類型轉換)時,對於編譯器是可知的,即編譯器知道對象的最終靜態類型。而實際類型變化(對象指向了其他對象)時,編譯器是不可知的,只有在運行時才可知。
//靜態類型變化
sr.sayHello((Man) man);
sr.sayHello((Woman) man);
//實際類型變化
Human man = new Man();
man = new Woman();
重載只涉及靜態類型的選擇。
測試代碼如下:
/**
* Created by fan on 2016/3/28.
*/
public class StaticDispatcher {
static class Human {}
static class Man extends Human {}
static class Woman extends Human {}
public void sayHello(Human human) {
System.out.println("Hello guy!");
}
public void sayHello(Man man) {
System.out.println("Hello man!");
}
public void sayHello(Woman woman) {
System.out.println("Hello woman!");
}
public static void main(String[] args) {
StaticDispatcher staticDispatcher = new StaticDispatcher();
Human man = new Man();
Human woman = new Woman();
staticDispatcher.sayHello(man);
staticDispatcher.sayHello(woman);
staticDispatcher.sayHello((Man)man);
staticDispatcher.sayHello((Woman)man);
}
}
先看看執行結果:
由此可見,當靜態類型發生變化時,會調用相應類型的方法。但是,當將Man強制類型轉換成Woman時,沒有編譯錯誤,卻有運行時異常。“classCastException”類映射異常。
看看字節碼指令:
javap -verbZ喎?http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vc2UgLWMgU3RhdGljRGlzcGF0Y2hlcjwvcD4NCjxwcmUgY2xhc3M9"brush:java;">
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=4, Args_size=1
0: new #7; //class StaticDispatcher
3: dup
4: invokespecial #8; //Method "
看到,在強制類型轉換時,會有指令checkCast的調用,而且invokevirtual指令的調用方法也發生了變化39: invokevirtual #14; //Method sayHello:(LStaticDispatcher$Man;)V
。
虛擬機(准確說是編譯器)在重載時是通過參數的靜態類型而不是實際類型作為判定依據的。
對於字面量類型,編譯器會自動進行類型轉換。轉換的順序為:
char-int-long-float-long-Character-Serializable-Object
轉換成Character是因為發生了自動裝箱,轉換成Serializable是因為Character實現了Serializable接口。
測試代碼如下:
/**
* Created by fan on 2016/3/29.
*/
public class DynamicDispatcher {
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");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
執行結果:
看下字節碼指令:
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=3, Args_size=1
0: new #2; //class DynamicDispatcher$Man
3: dup
4: invokespecial #3; //Method DynamicDispatcher$Man."":()V
7: astore_1
8: new #4; //class DynamicDispatcher$Woman
11: dup
12: invokespecial #5; //Method DynamicDispatcher$Woman."":()V
15: astore_2
16: aload_1
17: invokevirtual #6; //Method DynamicDispatcher$Human.sayHello:()V
20: aload_2
21: invokevirtual #6; //Method DynamicDispatcher$Human.sayHello:()V
24: new #4; //class DynamicDispatcher$Woman
27: dup
28: invokespecial #5; //Method DynamicDispatcher$Woman."":()V
31: astore_1
32: aload_1
33: invokevirtual #6; //Method DynamicDispatcher$Human.sayHello:()V
36: return
從字節碼中可以看到,他們調用的都是相同的方法invokevirtual #6; //Method DynamicDispatcher$Human.sayHello:()V
,但是執行的結果卻顯示調用了不同的方法。因為,在編譯階段,編譯器只知道對象的靜態類型,而不知道實際類型,所以在class文件中只能確定要調用父類的方法。但是在執行時卻會判斷對象的實際類型。如果實際類型實現這個方法,則直接調用,如果沒有實現,則按照繼承關系從下往上一次檢索,只要檢索到就調用,如果始終沒有檢索到,則拋異常(難道能編譯通過)。
(1)測試代碼如下:
/**
* Created by fan on 2016/3/29.
*/
public class Test {
static class Human {
protected void sayHello() {
System.out.println("Human say hello");
}
protected void sayHehe() {
System.out.println("Human say hehe");
}
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("Man say hello");
}
// protected void sayHehe() {
// System.out.println("Man say hehe");
// }
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("Woman say hello");
}
// protected void sayHehe() {
// System.out.println("Woman say hehe");
// }
}
public static void main(String[] args) {
Human man = new Man();
man.sayHehe();
}
}
測試結果如下:
字節碼指令:
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=2, Args_size=1
0: new #2; //class Test$Man
3: dup
4: invokespecial #3; //Method Test$Man."":()V
7: astore_1
8: aload_1
9: invokevirtual #4; //Method Test$Human.sayHehe:()V
12: return
字節碼指令與上面代碼的字節碼指令沒有本質區別。
(2)測試代碼如下:
/**
* Created by fan on 2016/3/29.
*/
public class Test {
static class Human {
protected void sayHello() {
}
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("Man say hello");
}
protected void sayHehe() {
System.out.println("Man say hehe");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("Woman say hello");
}
protected void sayHehe() {
System.out.println("Woman say hehe");
}
}
public static void main(String[] args) {
Human man = new Man();
man.sayHehe();
}
}
編譯時報錯:
這個例子說明了:Java編譯器是基於靜態類型進行檢查的。
修改上面錯誤代碼,如下所示:
/**
* Created by fan on 2016/3/29.
*/
public class Test {
static class Human {
protected void sayHello() {
System.out.println("Human say hello");
}
// protected void sayHehe() {
// System.out.println("Human say hehe");
// }
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("Man say hello");
}
protected void sayHehe() {
System.out.println("Man say hehe");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("Woman say hello");
}
protected void sayHehe() {
System.out.println("Woman say hehe");
}
}
public static void main(String[] args) {
Man man = new Man();
man.sayHehe();
}
}
注意在Main方法中,改成了Man man = new Man();
執行結果如下所示:
字節碼指令如下所示:
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=2, Args_size=1
0: new #2; //class Test$Man
3: dup
4: invokespecial #3; //Method Test$Man."":()V
7: astore_1
8: aload_1
9: invokevirtual #4; //Method Test$Man.sayHehe:()V
12: return
注意上面的字節碼指令invokevirtual #4; //Method Test$Man.sayHehe:()V
。
本文討論了一下重載與重寫的基本原理,查看了相關的字節碼指令,下篇博文 java方法調用之單分派與多分派(二)討論下單分派與多分派。