一個很典型的泛型(generic)代碼。T是類型變量,可以是任何引用類型:
public class Pair<T>{ private T first=null; private T second=null; public Pair(T fir,T sec){ this.first=fir; this.second=sec; } public T getFirst(){ return this.first; } public T getSecond(){ return this.second; } public void setFirst(T fir){ this.first=fir; } }
1、Generic class 創建對象
Pair<String> pair1=new Pair("string",1); ...① Pair<String> pair2=new Pair<String>("string",1) ...②
有個很有趣的現象: ①代碼在編譯期不會出錯,②代碼在編譯期會檢查出錯誤。
這個問題其實很簡單
(1) JVM本身並沒有泛型對象這樣的一個特殊概念。所有的泛型類對象在編譯器會全部變成普通類對象(這一點會在下面詳細闡述)。
比如①,②兩個代碼編譯器全部調用的是 Pair(Object fir, Object sec)這樣的構造器。
因此代碼①中的new Pair("string",1)在編譯器是沒有問題的,畢竟編譯器並不知道你創建的Pair類型中具體是哪一個類型變量T,而且編譯器肯定了String對象和Integer對象都屬於Object類型的。
但是一段運行pair1.getSecond()就會拋出ClassCastException異常。這是因為JVM會根據第一個參數"string"推算出T類型變量是String類型,這樣getSecond也應該是返回String類型,然后編譯器已經默認了second的操作數是一個值為1的Integer類型。當然就不符合JVM的運行要求了,不終止程序才怪。
(2) 但代碼②會在編譯器報錯,是因為new Pair<String>("string",1)已經指明了創建對象pair2的類型變量T應該是String的。所以在編譯期編譯器就知道錯誤出在第二個參數Integer了。
小結一下:
創建泛型對象的時候,一定要指出類型變量T的具體類型。爭取讓編譯器檢查出錯誤,而不是留給JVM運行的時候拋出異常。
2、JVM如何理解泛型概念 —— 類型擦除
事實上,JVM並不知道泛型,所有的泛型在編譯階段就已經被處理成了普通類和方法。
處理方法很簡單,我們叫做類型變量T的擦除(erased) 。
無論我們如何定義一個泛型類型,相應的都會有一個原始類型被自動提供。原始類型的名字就是擦除類型參數的泛型類型的名字。
如果泛型類型的類型變量沒有限定(<T>) ,那么我們就用Object作為原始類型;
如果有限定(<T extends XClass>),我們就XClass作為原始類型;
如果有多個限定(<T extends XClass1&XClass2>),我們就用第一個邊界的類型變量XClass1類作為原始類型;
比如上面的Pair<T>例子,編譯器會把它當成被Object原始類型替代的普通類來替代。
//編譯階段:類型變量的擦除 public class Pair{ private Object first=null; private Object second=null; public Pair(Object fir,Object sec){ this.first=fir; this.second=sec; } public Object getFirst(){ return this.first; } public void setFirst(Object fir){ this.first=fir; } }
3、泛型約束和局限性—— 類型擦除所帶來的麻煩
(1) 繼承泛型類型的多態麻煩。(—— 子類沒有覆蓋住父類的方法 )
看看下面這個類SonPair
class SonPair extends Pair<String>{ public void setFirst(String fir){....} }
很明顯,程序員的本意是想在SonPair類中覆蓋父類Pair<String>的setFirst(T fir)這個方法。但事實上,SonPair中的setFirst(String fir)方法根本沒有覆蓋住Pair<String>中的這個方法。
原因很簡單,Pair<String>在編譯階段已經被類型擦除為Pair了,它的setFirst方法變成了setFirst(Object fir)。 那么SonPair中setFirst(String)當然無法覆蓋住父類的setFirst(Object)了。
這對於多態來說確實是個不小的麻煩,我們看看編譯器是如何解決這個問題的。
編譯器 會自動在 SonPair中生成一個橋方法(bridge method ) : public void setFirst(Object fir){ setFirst((String) fir) }
這樣,SonPair的橋方法確實能夠覆蓋泛型父類的setFirst(Object) 了。而且橋方法內部其實調用的是子類字節setFirst(String)方法。對於多態來說就沒問題了。
問題還沒有完,多態中的方法覆蓋是可以了,但是橋方法卻帶來了一個疑問:
現在,假設 我們還想在 SonPair 中覆蓋getFirst()方法呢?
class SonPair extends Pair<String>{ public String getFirst(){....} }
由於需要橋方法來覆蓋父類中的getFirst,編譯器會自動在SonPair中生成一個 public Object getFirst()橋方法。
但是,疑問來了,SonPair中出現了兩個方法簽名一樣的方法(只是返回類型不同):
①String getFirst() // 自己定義的方法 ②Object getFirst() // 編譯器生成的橋方法
難道,編譯器允許出現方法簽名相同的多個方法存在於一個類中嗎?
事實上有一個知識點可能大家都不知道:
① 方法簽名 確實只有方法名+參數列表 。這毫無疑問!
② 我們絕對不能編寫出方法簽名一樣的多個方法 。如果這樣寫程序,編譯器是不會放過的。這也毫無疑問!
③ 最重要的一點是:JVM會用參數類型和返回類型來確定一個方法。 一旦編譯器通過某種方式自己編譯出方法簽名一樣的兩個方法(只能編譯器自己來創造這種奇跡,我們程序員卻不能人為的編寫這種代碼)。JVM還是能夠分清楚這些方法的,前提是需要返回類型不一樣。
(2) 泛型類型中的方法沖突
//在上面代碼中加入equals方法 public class Pair<T>{ public boolean equals(T value){ return (first.equals(value)); } }
這樣看似乎沒有問題的代碼連編譯器都通過不了:
【Error】 Name clash: The method equals(T) of type Pair<T> has the same erasure as equals(Object) of type Object but does not override it。
編譯器說你的方法與Object中的方法沖突了。這是為什么?
開始我也不太明白這個問題,覺得好像編譯器幫助我們使得equals(T)這樣的方法覆蓋上了Object中的equals(Object)。經過大家的討論,我覺得應該這么解釋這個問題?
首先、我們都知道子類方法要覆蓋,必須與父類方法具有相同的方法簽名(方法名+參數列表)。而且必須保證子類的訪問權限>=父類的訪問權限。這是大家都知道的事實。
然后、在上面的代碼中,當編譯器看到Pair<T>中的equals(T)方法時,第一反應當然是equals(T)沒有覆蓋住父類Object中的equals(Object)了。
接着、編譯器將泛型代碼中的T用Object替代(擦除)。突然發現擦除以后equals(T)變成了equals(Object),糟糕了,這個方法與Object類中的equals一樣了。基於開始確定沒有覆蓋這樣一個想法,編譯器徹底的瘋了(精神分裂)。然后得出兩個結論:①堅持原來的思想:沒有覆蓋。但現在一樣造成了方法沖突了。 ②寫這程序的程序員瘋了(哈哈)。
再說了,拿Pair<T>對象和T對象比較equals,就像牛頭對比馬嘴,哈哈,邏輯上也不通呀。
(3) 沒有泛型數組一說
Pair<String>[] stringPairs=new Pair<String>[10]; Pair<Integer>[] intPairs=new Pair<Integer>[10];
這種寫法編譯器會指定一個Cannot create a generic array of Pair<String>的錯誤
我們說過泛型擦除之后,Pair<String>[]會變成Pair[],進而又可以轉換為Object[];
假設泛型數組存在,那么
Object[0]=stringPairs[0]; Ok
Object[1]=intPairs[0]; Ok
這就麻煩了,理論上將Object[]可以存儲所有Pair對象,但這些Pair對象是泛型對象,他們的類型變量都不一樣,那么調用每一個Object[]數組元素的對象方法可能都會得到不同的記過,也許是個字符串,也許是整形,這對於JVM可是無法預料的。
記住: 數組必須牢記它的元素類型,也就是所有的元素對象都必須一個樣,泛型類型恰恰做不到這一點。即使Pair<String>,Pair<Integer>... 都是Pair類型的,但他們還是不一樣。
總結:泛型代碼與JVM
① 虛擬機中沒有泛型,只有普通類和方法。
② 在編譯階段,所有泛型類的類型參數都會被Object或者它們的限定邊界來替換。(類型擦除)
③ 在繼承泛型類型的時候,橋方法的合成是為了避免類型變量擦除所帶來的多態災難。