標籤:too tle 儲存 代碼 eth extend 實現 包含 start
【深入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
- 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的重載方法去執行之前,先看如下代碼:
Human man = new Man(); 我們把上面代碼中的“Human”稱為變數的靜態類型,後面的“Man”稱為變數的實際類型。靜態類型和實際類型在程式中都可以發生一些變化,區別是靜態類型的變化僅僅在使用時發生,變數本身的靜態類型不會被改變,並且最終的靜態類型是在編譯期可知的,而實際類型變化的結果在運行期才可確定。 回到上面的程式碼分析中,在調用say()方法時,方法的調用者(回憶上面關於宗量的定義,方法的調用者屬於宗量)都為sp的前提下,使用哪個重載版本,完全取決於傳入參數的數量和資料類型(方法的參數也是資料宗量)。代碼中刻意定義了兩個靜態類型相同、實際類型不同的變數,可見
編譯器(不是虛擬機器,因為如果是根據靜態類型做出的判斷,那麼在編譯期就確定了)在重載時是通過參數的靜態類型而不是實際類型作為判定依據的。並且靜態類型是編譯期可知的,所以在編譯階段,Javac編譯器就根據參數的靜態類型決定使用哪個重載版本。這就是靜態指派最典型的應用。
動態指派 動態指派與多態性的另一個重要體現——方法覆寫有著很緊密的關係。向上轉型後調用子類覆寫的方法便是一個很好地說明動態指派的例子。這種情況很常見,因此這裡不再用樣本程式進行分析。很顯然,在判斷執行父類中的方法還是子類中覆蓋的方法時,如果用靜態類型來判斷,那麼無論怎麼進行向上轉型,都只會調用父類中的方法,但實際情況是,根據對父類執行個體化的子類的不同,調用的是不同子類中覆寫的方法,很明顯,這裡是要根據變數的實際類型來指派方法的執行版本的。而實際類型的確定需要在程式運行時才能確定下來,這種在運行期根據實際類型確定方法執行版本的指派過程稱為動態指派。
單指派和多指派
前面給出:方法的接受者(亦即方法的調用者)與方法的參數統稱為方法的宗量。但指派是根據一個宗量對目標方法進行選擇,多指派是根據多於一個宗量對目標方法進行選擇。
為了方便理解,下面給出一段範例程式碼:
[java] view plain copy
- 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)是一門靜態多指派、動態單指派的語言。
Java虛擬機器 - 多態性實現機制