1. 程式人生 > >泛型 Generic 類型擦除引起的問題及解決方法

泛型 Generic 類型擦除引起的問題及解決方法

rabl -s ech ole exce lean com extends 程序

參考:http://blog.csdn.net/lonelyroamer/article/details/7868820#comments
因為種種原因,Java不能實現真正的泛型,只能使用類型擦除來實現偽泛型,這樣雖然不會有類型膨脹的問題,但是也引起了許多新的問題。所以,Sun對這些問題作出了許多限制,避免我們犯各種錯誤。

1、先檢查,再編譯,以及檢查編譯的對象和引用傳遞的問題

既然說類型變量會在編譯的時候擦除掉,那為什麽我們往ArrayList<String> arrayList=new ArrayList<String>();所創建的數組列表arrayList中,不能使用add方法添加整形呢?不是說泛型變量Integer會在編譯時候擦除變為原始類型Object嗎,為什麽不能存別的類型呢?既然類型擦除了,如何保證我們只能使用泛型變量限定的類型呢?
java是如何解決這個問題的呢?java編譯器是通過先檢查代碼中泛型的類型,然後再進行類型擦除,
進行編譯的。
舉個例子說明:
public static  void main(String[] args) {  
    ArrayList<String> arrayList=new ArrayList<String>();  
    arrayList.add("123");  
    arrayList.add(123);//編譯錯誤  
} 
在上面的程序中,使用add方法添加一個整形,在eclipse中,直接就會報錯,說明這就是在編譯之前的檢查。因為如果是在編譯之後檢查,類型擦除後,原始類型為Object,是應該運行任意引用類型的添加的。可實際上卻不是這樣,這恰恰說明了關於泛型變量的使用,是會在編譯之前檢查的。

那麽,這麽類型檢查是針對誰的呢?我們先看看參數化類型與原始類型的兼容
以ArrayList舉例子
//以前的寫法:
ArrayList arrayList = new ArrayList();
//現在的寫法:
ArrayList<String> arrayList = new ArrayList<String>();
如果是與以前的代碼兼容,各種引用傳值之間,必然會出現如下的情況:
ArrayList<String> arrayList1 = new ArrayList(); //可以實現與完全【使用泛型參數】一樣的效果
arrayList1.add("1");//編譯通過
arrayList1.add(1);//編譯錯誤
String str = arrayList1.get(0);//返回類型就是String  

ArrayList arrayList2 = new ArrayList<String>();//可以實現與完全【不使用泛型參數】一樣的效果
arrayList2.add(1);//編譯通過
Object object = arrayList2.get(0);//返回類型就是Object  

new ArrayList<String>().add("11");//編譯通過
new ArrayList<String>().add(22);//編譯錯誤
String string = new ArrayList<String>().get(0);//返回類型就是String
這樣寫都是沒有錯誤的,不過都會有個編譯時警告。
arrayList1的警告為:
Type safety: The expression of type ArrayList needs unchecked conversion to conform to ArrayList<String> .
類型安全性:ArrayList類型的表達式需要未經檢查的轉換才能符合ArrayList <String>
arrayList2的警告為:
ArrayList is a raw type. References to generic type ArrayList<E> should be parameterized .
ArrayList是一個原始類型。 對泛型類型ArrayList <E>的引用應該被參數化

所以上述arrayList1可以實現與完全使用泛型參數一樣的效果,而arrayList2則完全沒效果。因為new ArrayList()只是在內存中開辟一個存儲空間,可以存儲任何的類型對象,而真正涉及類型檢查的是它的引用,因為我們是使用它的引用來調用它的方法,比如說調用add()方法。
通過上面的例子,我們可以明白,類型檢查就是針對引用的,用這個引用調用泛型方法,就會對這個引用調用的方法進行類型檢測,而無關它真正引用的對象

從這裏,我們可以再討論下泛型中參數化類型為什麽不考慮繼承關系
在Java中,像下面形式的引用傳遞是不允許的:
ArrayList<String> arrayList=new ArrayList<Object>();//編譯錯誤  Type mismatch: cannot convert from ArrayList<Object> to ArrayList<String>
ArrayList<Object> arrayList=new ArrayList<String>();//編譯錯誤  Type mismatch: cannot convert from ArrayList<String> to ArrayList<Object>

我們先看第一種情況,將第一種情況拓展成下面的形式:
ArrayList<Object> arrayList = new ArrayList<Object>();
ArrayList<String> arrayList2 = arrayList;//編譯錯誤  Type mismatch: cannot convert from ArrayList<Object> to ArrayList<String>
我們先假設第二行代碼編譯沒錯。那麽當我們使用arrayList2引用用get()方法取值的時候,返回的都是String類型的對象(上面提到了,類型檢測是根據引用來決定的),可是它裏面實際上存放的是 Object 類型的對象,這樣,就會有ClassCastException了。所以為了避免這種極易出現的錯誤,Java不允許進行這樣的引用傳遞。這也是泛型出現的原因,就是為了解決類型轉換的問題,我們不能違背它的初衷。

再看第二種情況,將第二種情況拓展成下面的形式:
ArrayList<String> arrayList = new ArrayList<String>();
ArrayList<Object> arrayList2 = arrayList;//編譯錯誤  Type mismatch: cannot convert from ArrayList<String> to ArrayList<Object>
沒錯,這樣的情況比第一種情況好的多,最起碼在我們用arrayList2取值的時候不會出現ClassCastException,因為是從String轉換為Object。可是,這樣做有什麽意義呢?泛型出現的原因,就是為了解決類型轉換的問題,我們使用了泛型,到頭來還是要自己強轉,違背了泛型設計的初衷,所以java不允許這麽幹!再說,你如果又用arrayList2往裏面add()新的對象,那麽到時候取得時候,我怎麽知道我取出來的到底是String類型的,還是Object類型的呢?

所以,要格外註意,泛型中的引用傳遞的問題。

2、自動類型轉換

因為類型擦除的問題,所以所有的泛型類型變量最後都會被替換為原始類型。這樣就引起了一個問題,既然都被替換為原始類型,那麽為什麽我們在獲取的時候,不需要進行強制類型轉換呢?看下ArrayList和get方法:
public E get(int index) {  
    RangeCheck(index);  
    return (E) elementData[index];  
} 
看以看到,在return之前,會根據泛型變量進行強轉。

3、類型擦除與多態的沖突和解決方法

現在有這樣一個泛型類:
class Pair<T> {
    private T value;
    public T getValue() {
        return value;
    }
    public void setValue(T value) {
        this.value = value;
    }
}
然後我們想要一個子類繼承它
class DateInter extends Pair<Date> {
    @Override
    public void setValue(Date value) {
        super.setValue(value);
    }

    @Override
    public Date getValue() {
        return super.getValue();
    }
}
在這個子類中,我們設定父類的泛型類型為Pair<Date>,在子類中,我們覆蓋了父類的兩個方法,我們的原意是這樣的:
將父類的泛型類型限定為Date,那麽父類裏面的兩個方法的參數都為Date類型,所以,我們在子類中重寫這兩個方法一點問題也沒有。實際上,從他們的@Override標簽中也可以看到,一點問題也沒有,實際上是這樣的嗎?
分析:
實際上,類型擦除後,父類的的泛型類型全部變為了原始類型Object,所以父類編譯之後會變成下面的樣子:
class Pair {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
} 
我們再來看子類重寫父類的 setValue 方法中參數的類型:父類的類型是Object,而子類的類型是Date,父類和子類中參數類型不一樣,這如果是在普通的繼承關系中,根本就不會是重寫,而是重載,也就不能使用@Override標簽我們在一個main方法測試一下:
DateInter dateInter = new DateInter();
dateInter.setValue(new Date());//編譯正確
dateInter.setValue(new Object());//編譯錯誤  The method setValue(Date) in the type DateInter is not applicable for the arguments (Object)
然而,從上面測試代碼可以看出,確是是重寫了,而不是重載了。因為如果是重載,那麽子類中肯定有兩個setValue方法,一個是參數Object類型,一個是參數Date類型,可是我們發現,根本就沒有這樣的一個子類繼承自父類的Object類型參數的方法。

為什麽會這樣呢?
後面省略1000字......

4、泛型類型變量不能是基本數據類型

不能用類型參數替換基本類型。就比如,沒有ArrayList<double>,只有ArrayList<Double>。因為當類型擦除後,ArrayList的原始類型變為Object,但是Object類型不能存儲double值,只能引用Double的值。

5、運行時類型查詢

舉個例子:
ArrayList<String> arrayList = new ArrayList<String>();
System.out.println(arrayList instanceof ArrayList<?>);//true
System.out.println(arrayList instanceof ArrayList);//true
//System.out.println(arrayList instanceof ArrayList<Object>);//編譯錯誤
//System.out.println(arrayList instanceof ArrayList<String>);//編譯錯誤 
下面的兩種方式會提示如下錯誤:
Cannot perform instanceof check against parameterized type ArrayList<Object/String>.
    無法對參數化類型ArrayList <Object/String>執行instanceof檢查。
Use the form ArrayList<?> instead since further generic type information will be erased at runtime
    使用形式ArrayList <?>,因為進一步的泛型類型信息將在運行時被擦除
因為類型擦除之後,ArrayList<String>只剩下原始類型,泛型信息String不存在了。所以,運行時進行類型查詢的時候,使用下面的兩種方式是錯誤的。

6、異常中使用泛型的問題

1、不能拋出也不能捕獲泛型類的對象。事實上,泛型類擴展Throwable都不合法。例如:下面的定義將不會通過編譯:
public class Problem<T> extends Exception{...}
為什麽不能擴展Throwable,因為異常都是在運行時捕獲和拋出的,而在編譯的時候,泛型信息全都會被擦除掉,那麽,假設上面的編譯可行,那麽,在看下面的定義:
try{
}catch(Problem<Integer> e1){
}catch(Problem<Number> e2){
}
類型信息被擦除後,那麽兩個地方的catch都變為原始類型Object,那麽也就是說,這兩個地方的catch變的一模一樣,就相當於下面的這樣
try{
}catch(Problem<Object> e1){
}catch(Problem<Object> e2){
}
這個當然就是不行的。就好比,catch兩個一模一樣的普通異常,不能通過編譯一樣:

2、不能在catch子句中使用泛型變量
public static <T extends Throwable> void doWork(Class<T> t){
    try{
    }catch(T e){ //編譯錯誤
    }
}
因為泛型信息在編譯的時候已經變為原始類型,也就是說上面的T會變為原始類型Throwable,那麽如果可以在catch子句中使用泛型變量,那麽,下面的定義呢:
public static <T extends Throwable> void doWork(Class<T> t){  
    try{
    }catch(T e){ //編譯錯誤
    }catch(IndexOutOfBounds e){
    }
} 
根據異常捕獲的原則,一定是子類在前面,父類在後面,那麽上面就違背了這個原則。即使你在使用該靜態方法的使用T是ArrayIndexOutofBounds,在編譯之後還是會變成Throwable,ArrayIndexOutofBounds是IndexOutofBounds的子類,違背了異常捕獲的原則。所以java為了避免這樣的情況,禁止在catch子句中使用泛型變量。

但是在異常聲明中可以使用類型變量。下面方法是合法的。
public static<T extends Throwable> void doWork(T t) throws T{  
    try{
    }catch(Throwable realCause){
    t.initCause(realCause);
    throw t;
    }
}  
上面的這樣使用是沒問題的。

7、數組

這個不屬於類型擦除引起的問題。
不能聲明參數化類型的數組。如:
Pair<String>[] table = new Pair<String>[10]; //Cannot create a generic array of Pair<String>
如果需要收集參數化類型對象,直接使用ArrayList:ArrayList<Pair<String>>最安全且有效。

8、泛型類型的實例化

不能實例化泛型類型。如,
T t= new T(); //ERROR  Cannot instantiate the type T

9、類型擦除後的沖突

當泛型類型被擦除後,創建條件不能產生沖突。如在Pair類中添加下面的equals方法:
public boolean equals(T value) {
    //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
    //名稱沖突:Pair <T>類型的方法equals(T)具有與Object類似的equals(Object)相同的擦除,但不覆蓋它
    return false;
}
擦除後方法 boolean equals(T) 變成了方法 boolean equals(Object) ,這與 Object 的 equals 方法是沖突的!當然,補救的辦法是重新命名引發錯誤的方法。
泛型規範說明提及另一個原則:"要支持擦除的轉換,需要強行制一個類或者類型變量不能同時成為兩個接口的子類,而這兩個子類是同一接品的不同參數化"
下面的代碼是非法的:
class A implements Comparable<Integer> {
    @Override
    public int compareTo(Integer o) {
        return 0;
    }
}

class B extends A implements Comparable<String> {
    //The interface Comparable cannot be implemented more than once with different arguments: Comparable<Integer> and Comparable<String>
    //接口Comparable不能使用不同的參數多次實現:可比較的<Calendar>和Comparable <GregorianCalendar>
}
B會實現Comparable<Integer>和Compable<String>,這是同一個接口的不同參數化實現。
這一限制與類型擦除的關系並不很明確。非泛型版本:
 class A implements Comparable {//Comparable is a raw type. References to generic type Comparable<T> should be parameterized
    @Override
    public int compareTo(Object o) {
        return 0;
    }
}

class B extends A implements Comparable {
}
是合法的。

10、泛型在靜態方法和靜態類中的問題

泛型類中的靜態方法和靜態變量不可以使用泛型類所聲明的泛型類型參數
public class Test<T> {
    public static T one;  //編譯錯誤  Cannot make a static reference to the non-static type T
    public static  T show(T one){ //編譯錯誤  Cannot make a static reference to the non-static type T
        return null;
    }
}
因為泛型類中的泛型參數的實例化是在定義對象的時候指定的,而靜態變量和靜態方法不需要使用對象來調用。對象都沒有創建,如何確定這個泛型參數是何種類型,所以當然是錯誤的。

但是要註意區分下面的一種情況:
public class Test<T> {
    public static <T >T show(T one){//這是正確的
        return null;
    }
}
因為這是一個泛型方法,在泛型方法中使用的T是自己在方法中定義的T,而不是泛型類中的T。

來自為知筆記(Wiz)

泛型 Generic 類型擦除引起的問題及解決方法