Class 文件的編譯過(guò)程中不包含傳統(tǒng)編譯中的連接步驟,一切方法調(diào)用在 Class 文件里面存儲(chǔ)的都只是符號(hào)引用,而不是方法在實(shí)際運(yùn)行時(shí)內(nèi)存布局中的入口地址。這個(gè)特性給 Java 帶來(lái)了更強(qiáng)大的動(dòng)態(tài)擴(kuò)展能力,使得可以在類運(yùn)行期間才能確定某些目標(biāo)方法的直接引用,稱為動(dòng)態(tài)連接,也有一部分方法的符號(hào)引用在類加載階段或第一次使用時(shí)轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為靜態(tài)解析。這在前面的“Java 內(nèi)存區(qū)域與內(nèi)存溢出”一文中有提到。
靜態(tài)解析成立的前提是:方法在程序真正執(zhí)行前就有一個(gè)可確定的調(diào)用版本,并且這個(gè)方法的調(diào)用版本在運(yùn)行期是不可改變的。換句話說(shuō),調(diào)用目標(biāo)在編譯器進(jìn)行編譯時(shí)就必須確定下來(lái),這類方法的調(diào)用稱為解析。
在 Java 語(yǔ)言中,符合“編譯器可知,運(yùn)行期不可變”這個(gè)要求的方法主要有靜態(tài)方法和私有方法兩大類,前者與類型直接關(guān)聯(lián),后者在外部不可被訪問(wèn),這兩種方法都不可能通過(guò)繼承或別的方式重寫出其他的版本,因此它們都適合在類加載階段進(jìn)行解析。
Java 虛擬機(jī)里共提供了四條方法調(diào)用字節(jié)指令,分別是:
只要能被 invokestatic 和 invokespecial 指令調(diào)用的方法,都可以在解析階段確定唯一的調(diào)用版本,符合這個(gè)條件的有靜態(tài)方法、私有方法、實(shí)例構(gòu)造器和父類方法四類,它們?cè)陬惣虞d時(shí)就會(huì)把符號(hào)引用解析為該方法的直接引用。這些方法可以稱為非虛方法(還包括 final 方法),與之相反,其他方法就稱為虛方法(final 方法除外)。這里要特別說(shuō)明下 final 方法,雖然調(diào)用 final 方法使用的是 invokevirtual 指令,但是由于它無(wú)法覆蓋,沒(méi)有其他版本,所以也無(wú)需對(duì)方發(fā)接收者進(jìn)行多態(tài)選擇。Java 語(yǔ)言規(guī)范中明確說(shuō)明了 final 方法是一種非虛方法。
解析調(diào)用一定是個(gè)靜態(tài)過(guò)程,在編譯期間就完全確定,在類加載的解析階段就會(huì)把涉及的符號(hào)引用轉(zhuǎn)化為可確定的直接引用,不會(huì)延遲到運(yùn)行期再去完成。而分派調(diào)用則可能是靜態(tài)的也可能是動(dòng)態(tài)的,根據(jù)分派依據(jù)的宗量數(shù)(方法的調(diào)用者和方法的參數(shù)統(tǒng)稱為方法的宗量)又可分為單分派和多分派。兩類分派方式兩兩組合便構(gòu)成了靜態(tài)單分派、靜態(tài)多分派、動(dòng)態(tài)單分派、動(dòng)態(tài)多分派四種分派情況。
所有依賴靜態(tài)類型來(lái)定位方法執(zhí)行版本的分派動(dòng)作,都稱為靜態(tài)分派,靜態(tài)分派的最典型應(yīng)用就是多態(tài)性中的方法重載。靜態(tài)分派發(fā)生在編譯階段,因此確定靜態(tài)分配的動(dòng)作實(shí)際上不是由虛擬機(jī)來(lái)執(zhí)行的。下面通過(guò)一段方法重載的示例程序來(lái)更清晰地說(shuō)明這種分派機(jī)制:
class Human{
}
class Man extends Human{
}
class Woman extends Human{
}
public class StaticPai{
public void say(Human hum){
System.out.println("I am human");
}
public void say(Man hum){
System.out.println("I am man");
}
public void say(Woman hum){
System.out.println("I am woman");
}
public static void main(String[] args){
Human man = new Man();
Human woman = new Woman();
StaticPai sp = new StaticPai();
sp.say(man);
sp.say(woman);
}
}
上面代碼的執(zhí)行結(jié)果如下:
I am human
I am human
以上結(jié)果的得出應(yīng)該不難分析。在分析為什么會(huì)選擇參數(shù)類型為 Human 的重載方法去執(zhí)行之前,先看如下代碼:
Human man = new Man();
我們把上面代碼中的“Human”稱為變量的靜態(tài)類型,后面的“Man”稱為變量的實(shí)際類型。靜態(tài)類型和實(shí)際類型在程序中都可以發(fā)生一些變化,區(qū)別是靜態(tài)類型的變化僅僅在使用時(shí)發(fā)生,變量本身的靜態(tài)類型不會(huì)被改變,并且最終的靜態(tài)類型是在編譯期可知的,而實(shí)際類型變化的結(jié)果在運(yùn)行期才可確定。
回到上面的代碼分析中,在調(diào)用 say()方法時(shí),方法的調(diào)用者(回憶上面關(guān)于宗量的定義,方法的調(diào)用者屬于宗量)都為 sp 的前提下,使用哪個(gè)重載版本,完全取決于傳入?yún)?shù)的數(shù)量和數(shù)據(jù)類型(方法的參數(shù)也是數(shù)據(jù)宗量)。代碼中刻意定義了兩個(gè)靜態(tài)類型相同、實(shí)際類型不同的變量,可見(jiàn)編譯器(不是虛擬機(jī),因?yàn)槿绻歉鶕?jù)靜態(tài)類型做出的判斷,那么在編譯期就確定了)在重載時(shí)是通過(guò)參數(shù)的靜態(tài)類型而不是實(shí)際類型作為判定依據(jù)的。并且靜態(tài)類型是編譯期可知的,所以在編譯階段,javac 編譯器就根據(jù)參數(shù)的靜態(tài)類型決定使用哪個(gè)重載版本。這就是靜態(tài)分派最典型的應(yīng)用。
動(dòng)態(tài)分派與多態(tài)性的另一個(gè)重要體現(xiàn)——方法覆寫有著很緊密的關(guān)系。向上轉(zhuǎn)型后調(diào)用子類覆寫的方法便是一個(gè)很好地說(shuō)明動(dòng)態(tài)分派的例子。這種情況很常見(jiàn),因此這里不再用示例程序進(jìn)行分析。很顯然,在判斷執(zhí)行父類中的方法還是子類中覆蓋的方法時(shí),如果用靜態(tài)類型來(lái)判斷,那么無(wú)論怎么進(jìn)行向上轉(zhuǎn)型,都只會(huì)調(diào)用父類中的方法,但實(shí)際情況是,根據(jù)對(duì)父類實(shí)例化的子類的不同,調(diào)用的是不同子類中覆寫的方法,很明顯,這里是要根據(jù)變量的實(shí)際類型來(lái)分派方法的執(zhí)行版本的。而實(shí)際類型的確定需要在程序運(yùn)行時(shí)才能確定下來(lái),這種在運(yùn)行期根據(jù)實(shí)際類型確定方法執(zhí)行版本的分派過(guò)程稱為動(dòng)態(tài)分派。
前面給出:方法的接受者(亦即方法的調(diào)用者)與方法的參數(shù)統(tǒng)稱為方法的宗量。但分派是根據(jù)一個(gè)宗量對(duì)目標(biāo)方法進(jìn)行選擇,多分派是根據(jù)多于一個(gè)宗量對(duì)目標(biāo)方法進(jìn)行選擇。
為了方便理解,下面給出一段示例代碼:
class Eat{
}
class Drink{
}
class Father{
public void doSomething(Eat arg){
System.out.println("爸爸在吃飯");
}
public void doSomething(Drink arg){
System.out.println("爸爸在喝水");
}
}
class Child extends Father{
public void doSomething(Eat arg){
System.out.println("兒子在吃飯");
}
public void doSomething(Drink arg){
System.out.println("兒子在喝水");
}
}
public class SingleDoublePai{
public static void main(String[] args){
Father father = new Father();
Father child = new Child();
father.doSomething(new Eat());
child.doSomething(new Drink());
}
}
運(yùn)行結(jié)果應(yīng)該很容易預(yù)測(cè)到,如下:
爸爸在吃飯
兒子在喝水
我們首先來(lái)看編譯階段編譯器的選擇過(guò)程,即靜態(tài)分派過(guò)程。這時(shí)候選擇目標(biāo)方法的依據(jù)有兩點(diǎn):一是方法的接受者(即調(diào)用者)的靜態(tài)類型是 Father 還是 Child,二是方法參數(shù)類型是 Eat 還是 Drink。因?yàn)槭歉鶕?jù)兩個(gè)宗量進(jìn)行選擇,所以 Java 語(yǔ)言的靜態(tài)分派屬于多分派類型。
再來(lái)看運(yùn)行階段虛擬機(jī)的選擇,即動(dòng)態(tài)分派過(guò)程。由于編譯期已經(jīng)了確定了目標(biāo)方法的參數(shù)類型(編譯期根據(jù)參數(shù)的靜態(tài)類型進(jìn)行靜態(tài)分派),因此唯一可以影響到虛擬機(jī)選擇的因素只有此方法的接受者的實(shí)際類型是 Father 還是 Child。因?yàn)橹挥幸粋€(gè)宗量作為選擇依據(jù),所以 Java 語(yǔ)言的動(dòng)態(tài)分派屬于單分派類型。
根據(jù)以上論證,我們可以總結(jié)如下:目前的 Java 語(yǔ)言(JDK1.6)是一門靜態(tài)多分派、動(dòng)態(tài)單分派的語(yǔ)言。
更多建議: