1. 程式人生 > >Java虛擬機 - 多態性實現機制

Java虛擬機 - 多態性實現機制

虛擬機 () fat 第一次 實際類型 私有方法 base 動態 技術分享

【深入Java虛擬機】之五:多態性實現機制——靜態分派與動態分派

方法解析

Class文件的編譯過程中不包含傳統編譯中的連接步驟,一切方法調用在Class文件裏面存儲的都只是符號引用,而不是方法在實際運行時內存布局中的入口地址。這個特性給Java帶來了更強大的動態擴展能力,使得可以在類運行期間才能確定某些目標方法的直接引用,稱為動態連接,也有一部分方法的符號引用在類加載階段或第一次使用時轉化為直接引用,這種轉化稱為靜態解析。這在前面的“Java內存區域與內存溢出”一文中有提到。

靜態解析成立的前提是:方法在程序真正執行前就有一個可確定的調用版本,並且這個方法的調用版本在運行期是不可改變的。換句話說,調用目標在編譯器進行編譯時就必須確定下來,這類方法的調用稱為解析。

在Java語言中,符合“編譯器可知,運行期不可變”這個要求的方法主要有靜態方法和私有方法兩大類,前者與類型直接關聯,後者在外部不可被訪問,這兩種方法都不可能通過繼承或別的方式重寫出其他的版本,因此它們都適合在類加載階段進行解析。

Java虛擬機裏共提供了四條方法調用字節指令,分別是:

  • invokestatic:調用靜態方法。
  • invokespecial:調用實例構造器<init>方法、私有方法和父類方法。
  • invokevirtual:調用所有的虛方法。
  • invokeinterface:調用接口方法,會在運行時再確定一個實現此接口的對象。
只要能被invokestatic和invokespecial指令調用的方法,都可以在解析階段確定唯一的調用版本,符合這個條件的有靜態方法、私有方法、實例構造器和父類方法四類,它們在類加載時就會把符號引用解析為該方法的直接引用。這些方法可以稱為非虛方法(還包括final方法),與之相反,其他方法就稱為虛方法(final方法除外)。這裏要特別說明下final方法,雖然調用final方法使用的是invokevirtual指令,但是由於它無法覆蓋,沒有其他版本,所以也無需對方發接收者進行多態選擇。Java語言規範中明確說明了final方法是一種非虛方法。 解析調用一定是個靜態過程,在編譯期間就完全確定,在類加載的解析階段就會把涉及的符號引用轉化為可確定的直接引用,不會延遲到運行期再去完成。而分派調用則可能是靜態的也可能是動態的,根據分派依據的宗量數(方法的調用者和方法的參數統稱為方法的宗量)又可分為單分派和多分派。兩類分派方式兩兩組合便構成了靜態單分派、靜態多分派、動態單分派、動態多分派四種分派情況。

靜態分派

所有依賴靜態類型來定位方法執行版本的分派動作,都稱為靜態分派,靜態分派的最典型應用就是多態性中的方法重載。靜態分派發生在編譯階段,因此確定靜態分配的動作實際上不是由虛擬機來執行的。下面通過一段方法重載的示例程序來更清晰地說明這種分派機制:

[java] view plain copy
  1. class Human{
  2. }
  3. class Man extends Human{
  4. }
  5. class Woman extends Human{
  6. }
  7. public class StaticPai{
  8. public void say(Human hum){
  9. System.out.println("I am human");
  10. }
  11. public void say(Man hum){
  12. System.out.println("I am man");
  13. }
  14. public void say(Woman hum){
  15. System.out.println("I am woman");
  16. }
  17. public static void main(String[] args){
  18. Human man = new Man();
  19. Human woman = new Woman();
  20. StaticPai sp = new StaticPai();
  21. sp.say(man);
  22. sp.say(woman);
  23. }
  24. }
上面代碼的執行結果如下:

I am human
I am human

以上結果的得出應該不難分析。在分析為什麽會選擇參數類型為Human的重載方法去執行之前,先看如下代碼:

Human man = new Man(); 我們把上面代碼中的“Human”稱為變量的靜態類型,後面的“Man”稱為變量的實際類型。靜態類型和實際類型在程序中都可以發生一些變化,區別是靜態類型的變化僅僅在使用時發生,變量本身的靜態類型不會被改變,並且最終的靜態類型是在編譯期可知的,而實際類型變化的結果在運行期才可確定。 回到上面的代碼分析中,在調用say()方法時,方法的調用者(回憶上面關於宗量的定義,方法的調用者屬於宗量)都為sp的前提下,使用哪個重載版本,完全取決於傳入參數的數量和數據類型(方法的參數也是數據宗量)。代碼中刻意定義了兩個靜態類型相同、實際類型不同的變量,可見編譯器(不是虛擬機,因為如果是根據靜態類型做出的判斷,那麽在編譯期就確定了)在重載時是通過參數的靜態類型而不是實際類型作為判定依據的。並且靜態類型是編譯期可知的,所以在編譯階段,Javac編譯器就根據參數的靜態類型決定使用哪個重載版本。這就是靜態分派最典型的應用。

動態分派

動態分派與多態性的另一個重要體現——方法覆寫有著很緊密的關系。向上轉型後調用子類覆寫的方法便是一個很好地說明動態分派的例子。這種情況很常見,因此這裏不再用示例程序進行分析。很顯然,在判斷執行父類中的方法還是子類中覆蓋的方法時,如果用靜態類型來判斷,那麽無論怎麽進行向上轉型,都只會調用父類中的方法,但實際情況是,根據對父類實例化的子類的不同,調用的是不同子類中覆寫的方法,很明顯,這裏是要根據變量的實際類型來分派方法的執行版本的。而實際類型的確定需要在程序運行時才能確定下來,這種在運行期根據實際類型確定方法執行版本的分派過程稱為動態分派。

單分派和多分派

前面給出:方法的接受者(亦即方法的調用者)與方法的參數統稱為方法的宗量。但分派是根據一個宗量對目標方法進行選擇,多分派是根據多於一個宗量對目標方法進行選擇。

為了方便理解,下面給出一段示例代碼: [java] view plain copy
  1. class Eat{
  2. }
  3. class Drink{
  4. }
  5. class Father{
  6. public void doSomething(Eat arg){
  7. System.out.println("爸爸在吃飯");
  8. }
  9. public void doSomething(Drink arg){
  10. System.out.println("爸爸在喝水");
  11. }
  12. }
  13. class Child extends Father{
  14. public void doSomething(Eat arg){
  15. System.out.println("兒子在吃飯");
  16. }
  17. public void doSomething(Drink arg){
  18. System.out.println("兒子在喝水");
  19. }
  20. }
  21. public class SingleDoublePai{
  22. public static void main(String[] args){
  23. Father father = new Father();
  24. Father child = new Child();
  25. father.doSomething(new Eat());
  26. child.doSomething(new Drink());
  27. }
  28. }
運行結果應該很容易預測到,如下: 爸爸在吃飯
兒子在喝水
我們首先來看編譯階段編譯器的選擇過程,即靜態分派過程。這時候選擇目標方法的依據有兩點:一是方法的接受者(即調用者)的靜態類型是Father還是Child,二是方法參數類型是Eat還是Drink。因為是根據兩個宗量進行選擇,所以Java語言的靜態分派屬於多分派類型。
再來看運行階段虛擬機的選擇,即動態分派過程。由於編譯期已經了確定了目標方法的參數類型(編譯期根據參數的靜態類型進行靜態分派),因此唯一可以影響到虛擬機選擇的因素只有此方法的接受者的實際類型是Father還是Child。因為只有一個宗量作為選擇依據,所以Java語言的動態分派屬於單分派類型。 技術分享 根據以上論證,我們可以總結如下:目前的Java語言(JDK1.6)是一門靜態多分派、動態單分派的語言。

Java虛擬機 - 多態性實現機制