深入談Java的多態機制

前言

從開始學面向對象,開始學java就在不斷被灌輸java幾大特性:封裝、繼承、多態。封裝有利於實現數據(狀態)的隱藏,讓對象的內聚性更強。繼承雖說一定程度上破壞了封裝,但實現了代碼的複用,是多態特性實現的基礎之一。多態讓java的方法調用功能更加豐富,更加靈活,但帶來了一定的負面作用,如可讀性變差,類之間的耦合性變強。以下重點說說多態的實現原理和如何破解多態的問題!

本以爲看了周志明的《深入理解java虛擬機》(第八章)之後,自己對於多態的問題都已經瞭若指掌,但昨天看了一篇博客之後發現依然暈頭,如果大家也有我這樣的自信,不妨也看看http://blog.csdn.net/thinkGhoster/article/details/2307001。下面就針對這篇博客的問題,結合周志明大神書中的例子做一個分析。

周大神的例子原文

代碼清單1:

public class Dispatch{
	
	static class QQ{
	
	}
	
	static class _360{
	
	}
	
	static class Father{
		public void hardChoice(QQ arg){
			System.out.println("Father choose QQ");
		}
		
		public void hardChoice(_360 arg){
			System.out.println("Father choose 360");
		}
	}
	
	static class Son extends Father{
		public void hardChoice(QQ arg){
			System.out.println("Son choose QQ");
		}
		
		public void hardChoice(_360 arg){
			System.out.println("Son choose 360");
		}
	}
	
	
	public static void main(String[] args){
		Father f = new Father();
		Father s = new Son();
		
		f.hardChoice(new QQ());
		s.hardChoice(new _360());
	}
}

一個很有趣的示例,接着前幾年3Q大戰的背景進行調侃的。這個實例的結果應該不難分析:

Father choose QQ
Son choose 360

在編譯階段,通過靜態綁定可以確定f調用的是QQ參數的方法,s調用的是_360參數的方法,就看對象的實際類型是父類的還是子類,很容易得出以上結論!

但這個代碼如果改一下

代碼清單2:

public class Dispatch{
	static class LiuMang{
	
	}
	
	static class QQ extends LiuMang{
	
	}
	
	static class _360 extends LiuMang{
	
	}
	
	static class Father{
		public void hardChoice(LiuMang arg){
			System.out.println("Father choose QQ");
		}
		
		public void hardChoice(_360 arg){
			System.out.println("Father choose 360");
		}
	}
	
	static class Son extends Father{
		public void hardChoice(QQ arg) {
			System.out.println("Son choose QQ");
		}
		
		public void hardChoice(_360 arg){
			System.out.println("Son choose 360");
		}
	}
	
	
	public static void main(String[] args){
		Father f = new Father();
		Father s = new Son();
		QQ l1 = new QQ();
		LiuMang l2 = new _360();
		f.hardChoice(l1);
		s.hardChoice(l2);
	}
}

結果又是什麼呢?

Father choose QQ
Father choose QQ

是不是有點出乎意料了。怎麼都在調用父類的方法,s明明是子類的對象啊!要徹底弄明白這個問題,需要從三個方面來做分析:分派、字節碼指令、方法表

先說分派(Dispatch),分派也可叫綁定(binding),根據時機來分可分爲靜態分派和動態分派,根據宗量數量來分可分爲單宗量分派和多宗量分派,組合起來有四種,靜態單宗量、靜態多宗量、動態單宗量、動態多宗量,這裏java只使用靜態多宗量和動態單宗量。

那麼宗量是啥玩意呢?可理解爲條件或者因子,影響分派的因素,比如方法調用者(靜態類型:引用)、方法接收者(實際類型:實例)、方法簽名等。

靜態分派:編譯時可以確定調用範圍,由靜態類型和方法簽名共同決定,因此叫做靜態多分派。

動態分派:運行時由實際的對象決定,僅僅由方法的接收者(實例)來確定,因此叫做動態單分派。


下面結合字節碼指令說明一下代碼清單1:



上圖“1”表示靜態類型的調用者,“2”表示方法簽名,也就是編譯器已經根據這兩個宗量選擇好了由哪個類(的引用)來調用哪個重載方法。這沒問題,但是當運行時,“1”就會由實際類型來代替了,具體是調用哪個類(的實例)的方法,根據實例類型爲準。

再看下代碼清單2的字節指令:



上圖“1”沒改變還是靜態引用的類型,2卻發生很大的變化,參數列表都變成了“流氓”,爲什麼呢?

因爲參數列表也是“靜態宗量”,編譯器也是根據靜態類型進行判斷的,編譯器無法知道實參的具體類型是什麼(是null也說不準啊),所以只能根據靜態類型做判斷了。

現在你是不是還有一個疑問,既然(s.hardChoice(l2);這條代碼)方法的實際接受者是Son,爲什麼還要調用父類的方法呢?

其實也不是調用父類的方法了,而是子類Son的方法,因爲Son繼承了Father的所有的虛方法(概念等會講),就會擁有這個方法,因爲沒有覆蓋這個方法,所以內容就跟父類方法完全一樣了,看起來是不是有點暈,沒關係,看一下虛方法表(僞表)就明白了。

Father methodTable[0]=hardChoice(com.sia.send.test.Dispatch$LiuMang)
Father methodTable[1]=hardChoice(com.sia.send.test.Dispatch$_360)
Father methodTable[2]=wait(long,int)
Father methodTable[3]=wait(long)
Father methodTable[4]=wait()
Father methodTable[5]=equals(java.lang.Object)
Father methodTable[6]=toString()
Father methodTable[7]=hashCode()
Father methodTable[8]=getClass()
Father methodTable[9]=notify()
Father methodTable[10]=notifyAll()
Son methodTable[0]=hardChoice(com.sia.send.test.Dispatch$LiuMang)
Son methodTable[1]=hardChoice(com.sia.send.test.Dispatch$_360)
Son methodTable[2]=hardChoice(com.sia.send.test.Dispatch$QQ)
Son methodTable[3]=wait(long,int)
Son methodTable[4]=wait(long)
Son methodTable[5]=wait()
Son methodTable[6]=equals(java.lang.Object)
Son methodTable[7]=toString()
Son methodTable[8]=hashCode()
Son methodTable[9]=getClass()
Son methodTable[10]=notify()
Son methodTable[11]=notifyAll()

父類有兩個hardChoice,但子類由三個,有兩個於父類一毛一樣,子類源碼中我們只定義兩個hardChioce,有一個繼承了父類的,那麼
Son methodTable[0]=hardChoice(com.sia.send.test.Dispatch$LiuMang)
這個一定是父類繼承過來的了,當然與父類的行爲一模一樣,所以即使在動態分派時調用該方法,看起來也像是調的父類的方法。
注:真正的虛方法表一般也會把父子類相同方法的索引一一對應,這樣在進行動態分派時可以只改變對象的接收者而不用再表中重新查詢一遍了,提高效率。


行文至此,幾乎所有問題都說清楚了,還有一個問題,什麼是虛方法,什麼是非虛方法。

從行爲上來說:

非虛方法不會產生動態分派,具有“編譯時可知,運行時不改變”的特點。包括靜態方法(invokestatic),構造方法(invokespecial),私有方法(invokespecial)和父類方法(invokespecial),還包括final方法(invokevirtual),因爲final方法不可被覆蓋,因此也不存在運行時多態(編譯時即可唯一確定),有人建議方法使用final修飾可以提高調用效率50%,就是這個原因,無需到子類的虛方法表中查詢是否覆蓋了本方法。

所謂的虛方法,就是使用invokevirtual指令調用的方法,具體就是非private非final實例方法,這種調用可能產生多態,調用效率也最低,但正是由於這個功能才使得java具有豐富的功能和靈活性。


最後再來段代碼清單3,由讀者去想結果:

public class Dispatch{
	static class LiuMang{
	
	}
	
	static class QQ extends LiuMang{
	
	}
	
	static class _360 extends LiuMang{
	
	}
	
	static class Father{
		public void hardChoice(Object arg) {
			System.out.println("Father choose Object");
		}

		public void hardChoice(LiuMang arg) {
			System.out.println("Father choose QQ");
		}
		
		public void hardChoice(_360 arg){
			System.out.println("Father choose 360");
		}
	}
	
	static class Son extends Father{
		public void hardChoice(QQ arg) {
			System.out.println("Son choose QQ");
		}
		
		public void hardChoice(_360 arg){
			System.out.println("Son choose 360");
		}
	}
	
	
	public static void main(String[] args){
		Father f = new Father();
		Father s = new Son();
		QQ l1 = new QQ();
		_360 l2 = new _360();
		f.hardChoice(l2);
		s.hardChoice(l1);
	}
}







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章