天天看點

【深入Java虛拟機】之五:多态性實作機制——靜态分派與動态分派方法解析靜态分派   動态分派  單分派和多分派

轉載請注明出處:http://blog.csdn.net/ns_code/article/details/17965867

方法解析

    Class檔案的編譯過程中不包含傳統編譯中的連接配接步驟,一切方法調用在Class檔案裡面存儲的都隻是符号引用,而不是方法在實際運作時記憶體布局中的入口位址。這個特性給Java帶來了更強大的動态擴充能力,使得可以在類運作期間才能确定某些目标方法的直接引用,稱為動态連接配接,也有一部分方法的符号引用在類加載階段或第一次使用時轉化為直接引用,這種轉化稱為靜态解析。這在前面的“Java記憶體區域與記憶體溢出”一文中有提到。

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

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

   Java虛拟機裡共提供了四條方法調用位元組指令,分别是:

  • invokestatic:調用靜态方法。
  • invokespecial:調用執行個體構造器<init>方法、私有方法和父類方法。
  • 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的重載方法去執行之前,先看如下代碼:

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虛拟機】之五:多态性實作機制——靜态分派與動态分派方法解析靜态分派   動态分派  單分派和多分派

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

繼續閱讀