方法解析
Class 文件的編譯過程中不包含傳統(tǒng)編譯中的連接步驟,一切方法調(diào)用在 Class 文件里面存儲的都只是符號引用柿菩,而不是方法在實際運行時內(nèi)存布局中的入口地址。這個特性給 Java 帶來了更強大的動態(tài)擴展能力,使得可以在類運行期間才能確定某些目標方法的直接引用,稱為動態(tài)連接尼酿,也有一部分方法的符號引用在類加載階段或第一次使用時轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為靜態(tài)解析植影。這在前面的“Java 內(nèi)存區(qū)域與內(nèi)存溢出”一文中有提到裳擎。
靜態(tài)解析成立的前提是:方法在程序真正執(zhí)行前就有一個可確定的調(diào)用版本,并且這個方法的調(diào)用版本在運行期是不可改變的思币。換句話說鹿响,調(diào)用目標在編譯器進行編譯時就必須確定下來羡微,這類方法的調(diào)用稱為解析。
在 Java 語言中惶我,符合“編譯器可知妈倔,運行期不可變”這個要求的方法主要有靜態(tài)方法和私有方法兩大類,前者與類型直接關(guān)聯(lián)绸贡,后者在外部不可被訪問盯蝴,這兩種方法都不可能通過繼承或別的方式重寫出其他的版本,因此它們都適合在類加載階段進行解析恃轩。
Java 虛擬機里共提供了四條方法調(diào)用字節(jié)指令结洼,分別是:
- invokestatic:調(diào)用靜態(tài)方法。
- invokespecial:調(diào)用實例構(gòu)造器<init style="box-sizing: border-box; padding: 0px; margin: 0px;">方法叉跛、私有方法和父類方法松忍。</init>
- invokevirtual:調(diào)用所有的虛方法。
- invokeinterface:調(diào)用接口方法筷厘,會在運行時再確定一個實現(xiàn)此接口的對象鸣峭。
只要能被 invokestatic 和 invokespecial 指令調(diào)用的方法,都可以在解析階段確定唯一的調(diào)用版本酥艳,符合這個條件的有靜態(tài)方法摊溶、私有方法、實例構(gòu)造器和父類方法四類充石,它們在類加載時就會把符號引用解析為該方法的直接引用莫换。這些方法可以稱為非虛方法(還包括 final 方法),與之相反骤铃,其他方法就稱為虛方法(final 方法除外)拉岁。這里要特別說明下 final 方法,雖然調(diào)用 final 方法使用的是 invokevirtual 指令惰爬,但是由于它無法覆蓋喊暖,沒有其他版本,所以也無需對方發(fā)接收者進行多態(tài)選擇撕瞧。Java 語言規(guī)范中明確說明了 final 方法是一種非虛方法陵叽。
解析調(diào)用一定是個靜態(tài)過程,在編譯期間就完全確定丛版,在類加載的解析階段就會把涉及的符號引用轉(zhuǎn)化為可確定的直接引用巩掺,不會延遲到運行期再去完成。而分派調(diào)用則可能是靜態(tài)的也可能是動態(tài)的页畦,根據(jù)分派依據(jù)的宗量數(shù)(方法的調(diào)用者和方法的參數(shù)統(tǒng)稱為方法的宗量)又可分為單分派和多分派胖替。兩類分派方式兩兩組合便構(gòu)成了靜態(tài)單分派、靜態(tài)多分派、動態(tài)單分派刊殉、動態(tài)多分派四種分派情況。
靜態(tài)分派
所有依賴靜態(tài)類型來定位方法執(zhí)行版本的分派動作州胳,都稱為靜態(tài)分派记焊,靜態(tài)分派的最典型應(yīng)用就是多態(tài)性中的方法重載。靜態(tài)分派發(fā)生在編譯階段栓撞,因此確定靜態(tài)分配的動作實際上不是由虛擬機來執(zhí)行的遍膜。下面通過一段方法重載的示例程序來更清晰地說明這種分派機制:
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)該不難分析。在分析為什么會選擇參數(shù)類型為 Human 的重載方法去執(zhí)行之前瓤湘,先看如下代碼:
Human man = new Man();
我們把上面代碼中的“Human”稱為變量的靜態(tài)類型瓢颅,后面的“Man”稱為變量的實際類型。靜態(tài)類型和實際類型在程序中都可以發(fā)生一些變化弛说,區(qū)別是靜態(tài)類型的變化僅僅在使用時發(fā)生挽懦,變量本身的靜態(tài)類型不會被改變,并且最終的靜態(tài)類型是在編譯期可知的木人,而實際類型變化的結(jié)果在運行期才可確定信柿。
回到上面的代碼分析中,在調(diào)用 say()方法時醒第,方法的調(diào)用者(回憶上面關(guān)于宗量的定義渔嚷,方法的調(diào)用者屬于宗量)都為 sp 的前提下,使用哪個重載版本稠曼,完全取決于傳入?yún)?shù)的數(shù)量和數(shù)據(jù)類型(方法的參數(shù)也是數(shù)據(jù)宗量)形病。代碼中刻意定義了兩個靜態(tài)類型相同、實際類型不同的變量霞幅,可見編譯器(不是虛擬機漠吻,因為如果是根據(jù)靜態(tài)類型做出的判斷,那么在編譯期就確定了)在重載時是通過參數(shù)的靜態(tài)類型而不是實際類型作為判定依據(jù)的蝗岖。并且靜態(tài)類型是編譯期可知的侥猩,所以在編譯階段,javac 編譯器就根據(jù)參數(shù)的靜態(tài)類型決定使用哪個重載版本抵赢。這就是靜態(tài)分派最典型的應(yīng)用欺劳。
動態(tài)分派
動態(tài)分派與多態(tài)性的另一個重要體現(xiàn)——方法覆寫有著很緊密的關(guān)系。向上轉(zhuǎn)型后調(diào)用子類覆寫的方法便是一個很好地說明動態(tài)分派的例子铅鲤。這種情況很常見划提,因此這里不再用示例程序進行分析。很顯然邢享,在判斷執(zhí)行父類中的方法還是子類中覆蓋的方法時鹏往,如果用靜態(tài)類型來判斷,那么無論怎么進行向上轉(zhuǎn)型骇塘,都只會調(diào)用父類中的方法伊履,但實際情況是韩容,根據(jù)對父類實例化的子類的不同,調(diào)用的是不同子類中覆寫的方法唐瀑,很明顯群凶,這里是要根據(jù)變量的實際類型來分派方法的執(zhí)行版本的。而實際類型的確定需要在程序運行時才能確定下來哄辣,這種在運行期根據(jù)實際類型確定方法執(zhí)行版本的分派過程稱為動態(tài)分派请梢。
單分派和多分派
前面給出:方法的接受者(亦即方法的調(diào)用者)與方法的參數(shù)統(tǒng)稱為方法的宗量。但分派是根據(jù)一個宗量對目標方法進行選擇力穗,多分派是根據(jù)多于一個宗量對目標方法進行選擇毅弧。
為了方便理解,下面給出一段示例代碼:
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());
}
}
運行結(jié)果應(yīng)該很容易預(yù)測到当窗,如下:
爸爸在吃飯
兒子在喝水
我們首先來看編譯階段編譯器的選擇過程够坐,即靜態(tài)分派過程。這時候選擇目標方法的依據(jù)有兩點:一是方法的接受者(即調(diào)用者)的靜態(tài)類型是 Father 還是 Child崖面,二是方法參數(shù)類型是 Eat 還是 Drink咆霜。因為是根據(jù)兩個宗量進行選擇,所以 Java 語言的靜態(tài)分派屬于多分派類型嘶朱。
再來看運行階段虛擬機的選擇蛾坯,即動態(tài)分派過程。由于編譯期已經(jīng)了確定了目標方法的參數(shù)類型(編譯期根據(jù)參數(shù)的靜態(tài)類型進行靜態(tài)分派)疏遏,因此唯一可以影響到虛擬機選擇的因素只有此方法的接受者的實際類型是 Father 還是 Child脉课。因為只有一個宗量作為選擇依據(jù),所以 Java 語言的動態(tài)分派屬于單分派類型财异。
根據(jù)以上論證倘零,我們可以總結(jié)如下:目前的 Java 語言(JDK1.6)是一門靜態(tài)多分派、動態(tài)單分派的語言戳寸。
原文:http://wiki.jikexueyuan.com/project/java-vm/polymorphism.html