多態性實現機制——靜態分派與動態分派

方法解析

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

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

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

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

  • invokestatic:調用靜態方法。
  • invokespecial:調用實例構造器方法、私有方法和父類方法。
  • invokevirtual:調用全部的虛方法。
  • invokeinterface:調用接口方法,會在運行時再肯定一個實現此接口的對象。

只要能被 invokestatic 和 invokespecial 指令調用的方法,均可以在解析階段肯定惟一的調用版本,符合這個條件的有靜態方法、私有方法、實例構造器和父類方法四類,它們在類加載時就會把符號引用解析爲該方法的直接引用。這些方法能夠稱爲非虛方法(還包括 final 方法),與之相反,其餘方法就稱爲虛方法(final 方法除外)。這裏要特別說明下 final 方法,雖然調用 final 方法使用的是 invokevirtual 指令,可是因爲它沒法覆蓋,沒有其餘版本,因此也無需對方發接收者進行多態選擇。Java 語言規範中明確說明了 final 方法是一種非虛方法。對象

解析調用必定是個靜態過程,在編譯期間就徹底肯定,在類加載的解析階段就會把涉及的符號引用轉化爲可肯定的直接引用,不會延遲到運行期再去完成。而分派調用則多是靜態的也多是動態的,根據分派依據的宗量數(方法的調用者和方法的參數統稱爲方法的宗量)又可分爲單分派和多分派。兩類分派方式兩兩組合便構成了靜態單分派、靜態多分派、動態單分派、動態多分派四種分派狀況。繼承

靜態分派

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

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);  
    }  
}

上面代碼的執行結果以下:內存

I am human
 I am human

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

Human man = new Man();

咱們把上面代碼中的「Human」稱爲變量的靜態類型,後面的「Man」稱爲變量的實際類型。靜態類型和實際類型在程序中均可以發生一些變化,區別是靜態類型的變化僅僅在使用時發生,變量自己的靜態類型不會被改變,而且最終的靜態類型是在編譯期可知的,而實際類型變化的結果在運行期纔可肯定。編譯器

回到上面的代碼分析中,在調用 say()方法時,方法的調用者(回憶上面關於宗量的定義,方法的調用者屬於宗量)都爲 sp 的前提下,使用哪一個重載版本,徹底取決於傳入參數的數量和數據類型(方法的參數也是數據宗量)。代碼中刻意定義了兩個靜態類型相同、實際類型不一樣的變量,可見編譯器(不是虛擬機,由於若是是根據靜態類型作出的判斷,那麼在編譯期就肯定了)在重載時是經過參數的靜態類型而不是實際類型做爲斷定依據的。而且靜態類型是編譯期可知的,因此在編譯階段,javac 編譯器就根據參數的靜態類型決定使用哪一個重載版本。這就是靜態分派最典型的應用。

動態分派

動態分派與多態性的另外一個重要體現——方法覆寫有着很緊密的關係。向上轉型後調用子類覆寫的方法即是一個很好地說明動態分派的例子。這種狀況很常見,所以這裏再也不用示例程序進行分析。很顯然,在判斷執行父類中的方法仍是子類中覆蓋的方法時,若是用靜態類型來判斷,那麼不管怎麼進行向上轉型,都只會調用父類中的方法,但實際狀況是,根據對父類實例化的子類的不一樣,調用的是不一樣子類中覆寫的方法,很明顯,這裏是要根據變量的實際類型來分派方法的執行版本的。而實際類型的肯定須要在程序運行時才能肯定下來,這種在運行期根據實際類型肯定方法執行版本的分派過程稱爲動態分派。

單分派和多分派

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

爲了方便理解,下面給出一段示例代碼:

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());  
    }  
}

運行結果應該很容易預測到,以下:

爸爸在吃飯
兒子在喝水

咱們首先來看編譯階段編譯器的選擇過程,即靜態分派過程。這時候選擇目標方法的依據有兩點:一是方法的接受者(即調用者)的靜態類型是 Father 仍是 Child,二是方法參數類型是 Eat 仍是 Drink。由於是根據兩個宗量進行選擇,因此 Java 語言的靜態分派屬於多分派類型。

再來看運行階段虛擬機的選擇,即動態分派過程。因爲編譯期已經了肯定了目標方法的參數類型(編譯期根據參數的靜態類型進行靜態分派),所以惟一能夠影響到虛擬機選擇的因素只有此方法的接受者的實際類型是 Father 仍是 Child。由於只有一個宗量做爲選擇依據,因此 Java 語言的動態分派屬於單分派類型。

根據以上論證,咱們能夠總結以下:目前的 Java 語言(JDK1.6)是一門靜態多分派、動態單分派的語言。

相關文章
相關標籤/搜索