泛型擦除
擦除的現象
當開始深入研究泛型的時,會發現其實有些東西是沒有意義的。例如,我們可以宣告ArrayList.class
,但是卻無法宣告ArrayList<Integer>.class
。
這是因為泛型的擦除機制造成的,考慮以下的情況。
public class ErasedTypeEquivalence { public static void main(String[] args) { Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); System.out.println("(c1 == c2) = " + (c1 == c2)); } }
以上程式碼中,表明ArrayList<String>
和ArrayList<Integer>
是同一型別。不同的型別在行為方面肯定不同。例如,如果試著將一個Integer
型別放入ArrayList<String>
,所得的行為和Integer
型別放入ArrayList<Integer>
完全不同,但是它們仍然是同一型別。
以下的程式碼是對這個問題的一個補充。
class Frob { } class Fnorkle { } class Quark<Q> { } class Particle<POSITION, MOMENTUM> { } public class LostInfomation { public static void main(String[] args) { List<Frob> list = new ArrayList<>(); Map<Frob,Fnorkle> map = new HashMap<>(); Quark<Fnorkle> quark = new Quark<>(); Particle<Long,Double> p = new Particle<>(); System.out.println(Arrays.toString( list.getClass().getTypeParameters() )); System.out.println(Arrays.toString( map.getClass().getTypeParameters() )); System.out.println(Arrays.toString( quark.getClass().getTypeParameters() )); System.out.println(Arrays.toString( p.getClass().getTypeParameters() )); } }
// Outputs
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
Class.getTypeParameters
是返回一個TypeVariable
物件陣列,表示泛型宣告所宣告的型別引數。但是上例的輸出也表明了,這個方法獲得的只是做引數佔位符的識別符號。
擦除的概念
在泛型程式碼內部,無法獲得任何有關泛型引數型別的資訊。
Java的泛型是使用擦除來實現的,這就意味著在使用泛型的時候,任何具體的型別資訊都會被擦除。寫程式碼時唯一知道就是在使用一個物件。因此,List<String>
和List<Integer>
在執行時事實上是相同的型別。這兩種形式都會被擦除成它們的"原生"型別,即List
。理解擦除以及應該如何處理它,是在學習Java泛型時候的最大阻礙。
因此,可以獲得型別引數識別符號和泛型型別邊界這樣的資訊,但是卻無法知道用來建立某個特定例項的實際的型別引數。
擦除的邊界
class HasF{
public void f(){
System.out.println("HasF.f()");
}
}
class Manipulator<T> {
private T obj;
public Manipulator(T obj) {
this.obj = obj;
}
public void manipulate(){
//obj.f() compile error
}
}
public class Manipulation {
public static void main(String[] args) {
HasF hf = new HasF();
Manipulator<HasF> manipulator = new Manipulator<>(hf);
manipulator.manipulate();
}
}
由於有了擦除機制,Java編譯器無法將manipulate()
必須能夠在obj
上呼叫f()
這一需求對映到HasF
擁有f()
這一事實上,為了呼叫f()
,我們必須協助泛型類,給定泛型類的邊界,以便告知編譯器只能遵循這個邊界的型別。這裡重用了extends
關鍵字。並由於有了邊界,下面的程式碼可以編譯了。
class Manipulator2<T extends HasF> {
private T obj;
public Manipulator2(T obj) {
this.obj = obj;
}
public void manipulate(){
obj.f();
}
}
上面的程式碼中,邊界<T extentds HasF>
宣告T
必須具有型別HasF
或者從HasF
匯出來的型別,因為這個約束,所以可以安全地在obj
上呼叫f了。
這裡說泛型的型別引數將擦除到它的第一邊界(泛型可能有多個邊界)。這裡提到了型別引數的擦除,編譯器實際上會把型別引數替換成它的擦除,就像上面的示例那樣,T
擦除到了HasF
,就像在類的宣告中用HasF
替換成T
一樣。
如同上文所說,我們可以不使用泛型,直接將T
替換回會HasF
。
class Manipulator3 {
private HasF obj;
public Manipulator2(HasF obj) {
this.obj = obj;
}
public void manipulate(){
obj.f();
}
}
上面的程式碼也可以像Manipulator2
中那樣正常工作。但是這並不意味著帶邊界的泛型是毫無意義的。
只有當希望使用的型別引數比某個具體型別(以及它的所有子型別)更加"泛化"時。也就是說,當希望程式碼能跨多個類工作的時候,使用泛型才有幫助。因此,型別引數和它們在有用的泛型程式碼中的應用,通常比簡單的類替換要更為複雜。。但是也不能因為覺得<T extends HasF>
的任何東西都是有缺陷的。
例如,假設某個類有返回T
的方法,那麼泛型在這裡就是有用處的,因為泛型可以返回確切的型別。例子如下。
class ReturnGenericType<T extends HasF> {
private T obj;
public ReturnGenericType(T obj) {
this.obj = obj;
}
public T getObj() {
return obj;
}
}
所以,必須檢視所有的程式碼。並確定它是否"足夠複雜"到必須使用泛型的程