Java是一門面向對象的語言,因為Java具備面向對象的三個特性:封裝、繼承、多态。分派的過程會揭示多态特性的一些最基本的展現,如“重載”和“重寫”在Java虛拟機中是如何實作的,并不是文法上如何寫,我們關心的依然是虛拟機如何确定正确的目标方法。
一、靜态分派
先看一段代碼
package cn.zjm.show.polymorphic;
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 man) {
System.out.println("hello,gentleman!");
}
public void sayHello(Woman woman) {
System.out.println("hello,lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch s = new StaticDispatch();
s.sayHello(man);
s.sayHello(woman);
}
}
運作結果:
hello,guy!
hello,guy!
相信對Java稍有了解的人都能想到正确的執行結果,但為什麼會選擇執行參數類型為Human的重載呢?先按如下代碼定義兩個重要的概念:
Human man = new Man();
我們把上面代碼的 Human 稱為變量的 靜态類型(Static Type),或者叫做 外觀類型(Apparent Type),後面的 Man 則稱之為變量的 實際類型(Actual Type),靜态類型和實際類型在程式中都可以發生一些變化,差別是 靜态類型的變化僅僅在使用時發生,變量本身的靜态類型不會被改變,并且最終的靜态類型是在編譯期可知的;而實際類型變化的結果在運作期才可确定,編譯器在編譯程式的時候并不知道一個對象的實際類型是什麼。例如下面的代碼:
//實際類型變化
Human man = new Man();
man = new Woman();
//靜态類型變化
s.sayHello((Man) man);
s.sayHello((Woman) man);
運作結果:
hello,gentleman!
hello,lady!
解釋了這兩個概念,回到第一段代碼中。main()裡面兩次sayHello()方法調用,在方法的接收者已經确定是對象 s 的前提下,使用哪個重載版本,就完全取決于 傳入參數的數量和資料類型。代碼刻意地定義了兩個靜态類型相同但是實際類型不同的變量,但編譯器在重載時是通過參數的靜态類型而不是實際類型作為判定依據的。并且靜态類型是編譯期可知的,是以,在編譯階段,Javac編譯器會根據參數的靜态類型決定使用哪個重載版本,是以選擇了 sayHello(Human) 最為調用目标。
二、動态分派
動态分派和多态性另一個重要展現——重寫(Override)有着很密切的關聯。我們還是用前面的Man和Woman一起sayHello的栗子來講解動态分派。
package cn.zjm.show.polymorphic;
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");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
運作結果:
man say hello
woman say hello
woman say hello
這個運作結果不會出乎任何人的預料,對于習慣了面向對象思維的Java程式員或覺得這是理所應當的。現在的問題還和前面的一樣,虛拟機是如何知道要調用哪個方法的?
顯然這裡不可能再根據靜态類型來決定,因為靜态類型同樣都是 Human 的兩個變量 man 和 woman 在調用 sayHello() 方法時執行了不同的行為,并且變量man在兩次調用中執行了不同的方法。導緻這個現象的原因很明顯,是這兩個變量的實際類型不同,Java虛拟機是如何根據實際類型來分派方法執行版本的呢?用javap進行反編譯 。
main()方法位元組碼:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class cn/zjm/show/polymorphic/DynamicDispatch$Man
3: dup
4: invokespecial #3 // Method cn/zjm/show/polymorphic/DynamicDispatch$Man."<init>":()V
7: astore_1
8: new #4 // class cn/zjm/show/polymorphic/DynamicDispatch$Woman
11: dup
12: invokespecial #5 // Method cn/zjm/show/polymorphic/DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method cn/zjm/show/polymorphic/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method cn/zjm/show/polymorphic/DynamicDispatch$Human.sayHello:()V
24: new #4 // class cn/zjm/show/polymorphic/DynamicDispatch$Woman
27: dup
28: invokespecial #5 // Method cn/zjm/show/polymorphic/DynamicDispatch$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6 // Method cn/zjm/show/polymorphic/DynamicDispatch$Human.sayHello:()V
36: return
LineNumberTable:
line 24: 0
line 25: 8
line 26: 16
line 27: 20
line 28: 24
line 29: 32
line 30: 36
}
0~15行的位元組碼是建立兩個對象的過程,分别調用了 Man 和 Woman 類型的執行個體構造器,将兩個執行個體的引用存放在1、2局部變量表Slot中,這兩個動作對應了代碼中的這兩句:
Human man = new Man();
Human woman = new Woman();
接下來16~21句是關鍵部分,16句和20句分别把剛剛建立的兩個對象的引用壓入棧頂,這兩個對象是将要執行sayHello()方法的所有者,稱為接收者(Receiver);17和21句是方法調用指令,這兩條調用指令但從位元組碼角度來看,無論是指令(都是invokevirtual)還是參數(都是常量池中第6項的常量,第六項常量為:#6 = Methodref #12.#25 // cn/zjm/show/polymorphic/DynamicDispatch$Human.sayHello:()V)都是完全一樣的,但是這兩句指令最終執行的目标方法并不相同。原因就需要從invokevirtual執行的多态查找過程開始說起,invokevirtual指令的運作時解析過程大緻分為以下幾個步驟:
1)找到操作數棧頂的第一個元素所指向的對象的實際類型,記作C。
2)如果在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行通路權限校驗,如果通過則傳回這個方法的直接引用,查找過程結束;如果不通過,則傳回java.langIllegalAccessError異常。
3)否則,按照繼承關系從下往上依此對C的各個父類進行第2步的搜尋和驗證過程。
4)如果始終沒有找到合适的方法,則抛出java,lang.AbstractMethodError異常。
由于invokevirtual指令執行的第一步就是在運作期确定接收者的實際類型,是以兩次調用中的invokevirtual指令把常量池中的類方法符号引用解析到了不同的直接引用上,這個過程就是Java語言中方法重寫的本質。我們把這種運作期根據實際類型确定方法執行版本的分派過程稱為動态分派。
靜态分派、動态分派、虛拟機、Java