在聊Java分派之前,大家不妨先來看以下代碼的運行結果
public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human guy) {
System.out.println("hello, guy");
}
public void sayHello(Man guy) {
System.out.println("hello, gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello, lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}
上述代碼的輸出結果是
hello, guy
hello, guy
我初次看到這段代碼的時候,怎麼也想不到這段代碼的結果會是如上結果。後來看了解析,發現這裏涉及到Java中的一個概念–分派。
何爲分派
Java具有面向對象的三個基本特徵:封裝、繼承和多態,而分派,是多態性特徵(如“重載”、“重寫”)在虛擬機層面的一種體現。具體來說,分派是虛擬機確定正確的目標方法的一個過程。
靜態分派和動態分派
首先來了解一下靜態類型和實際類型這兩個概念
Map map = new HashMap<String, String>();
上例中的Map叫靜態類型/外觀類型,HashMap叫做實際類型。可以看出來,靜態類型一般爲抽象類/基類/接口,實際類型一般爲子類/實現類。
靜態類型和實際類型在程序中都可以發生一些變化, 區別是靜態類型的變化僅僅在使用時發生, 變量本身的靜態類型不會被改變, 並且最終的靜態類型是在編譯期可知的; 而實際類型變化的結果在運行期纔可確定, 編譯器在編譯程序的時候並不知道一個對象的實際類型是什麼。
- 靜態分派
所有依賴靜態類型來定位方法執行版本的分派動作稱爲靜態分派。
靜態分派的典型應用是方法重載。當在一個發生重載的方法中尋找最匹配的執行方法時,會優先根據靜態類型來選擇方法。現在再回頭看我們的引導案例,實際執行方法爲什麼是*sayHello(Human guy)*就一目瞭然了。
靜態分派發生在編譯階段,因此確定靜態分派的動作實際上不是由虛擬機來執行的。
另外,編譯器雖然能確定出方法的重載版本,但在很多情況下這個重載版本並不是“唯一的”,往往只能確定一個“更加合適的”版本,大家可以嘗試運行一下以下程序,依次註釋掉最匹配的結果,看看會打印出什麼結果。
public class Overload {
public static void sayHello(Object arg) {
System.out.println("Hello Object");
}
public static void sayHello(int arg) {
System.out.println("Hello int");
}
public static void sayHello(long arg) {
System.out.println("Hello long");
}
public static void sayHello(Character arg) {
System.out.println("Hello character");
}
public static void sayHello(char arg) {
System.out.println("Hello char");
}
public static void sayHello(char... arg) {
System.out.println("Hello char...");
}
public static void sayHello(Serializable arg) {
System.out.println("Hello serializable");
}
public static void main(String[] args) {
sayHello('a');
}
}
- 動態分派
在運行期根據實際類型確定方法執行版本的分派過程稱爲動態分派。
我們來看一個動態分派的案例。
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("hello, man");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("hello, woman");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
}
}
相信稍微瞭解一點Java的人都能回答出來,上面代碼的運行結果是
hello, man
hello, woman
這是一個明顯的Java重寫方法案例,實際上,這個就是動態分派。只有在運行時,虛擬機才知道真正的執行主體是誰。
我們使用javap來分析一下上述代碼的彙編指令
可以看到,最終都是要執行虛方法
invokevirtual #6 // Method cn/hewie/dispatch/dynamicdispatch/DynamicDispatch$Human.sayHello:()V
這裏的sayHello方法已經在編譯階段確定了,但是其執行的主體是從"aload_1"、"aload_2"這樣的變量中獲取的,這裏的值分別在上面的代碼中設置爲了Man和Women,所以產生這樣的運行結果。動態分派裏的“動態”就是這樣體現出來的,這也是重寫的在虛擬機層面的一種體現。
單分派和多分派
方法的接收者與方法的參數統稱爲方法的宗量。根據分派基於多少種宗量, 可以將分派劃分爲單分派和多分派兩種。 單分派是根據一個宗量對目標方法進行選擇, 多分派則是根據多個宗量對目標方法進行選擇。
總結
網上常說Java語言是一門靜態多分派、 動態單分派的語言,具體來說是因爲
- Java中方法的執行要經歷兩個過程的選擇,一個是編譯期的靜態分派,一個是運行期的動態分派。這種分派場景出現的根本原因是多態。
- 而一個方法需要確定的宗量有兩個,一個是方法的接受者(調用者),一個是方法的參數。
- 進行編譯期的編譯時,需要確定的參數有兩個,一個是接受者,一個是參數,所以此時是多分派的。
- 編譯之後,方法的參數就已經確定了。等進入到運行期,此時宗量就只剩下方法的接受者了,所以此時是單分派的。
- 確定方法參數的時候,能夠影響到方法選擇的,就是方法的重載。
- 確定方法接受者的時候,能夠影響到方法選擇的,就是方法的重寫。
參考文檔:
深入理解Java虛擬機:JVM高級特性與最佳實踐 周志明 著