Java的泛型擦除和運行時泛型信息獲取

Java 的泛型擦除和泛型信息獲取

Java 的泛型擦除

擦除

Class c1 = new ArrayList<Integer>().getClass();  
Class c2 = new ArrayList<String>().getClass();   
System.out.println(c1 == c2);  
  
/* Output 
true 
*/  

ArrayList<Integer> 和 ArrayList<String> 在編譯的時候是完全不同的類型。你無法在寫代碼時,把一個 String 類型的實例加到 ArrayList<Integer> 中。但是在程序運行時,的的確確會輸出true。
這就是 Java 泛型的類型擦除造成的,因爲不管是 ArrayList<Integer> 還是 ArrayList<String>,在編譯時都會被編譯器擦除成了 ArrayList。Java 之所以要避免在創建泛型實例時而創建新的類,從而避免運行時的過度消耗。

List<Integer> list = new ArrayList<Integer>();  
Map<Integer, String> map = new HashMap<Integer, String>();  
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));  
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));  
  
/* Output 
[E] 
[K, V] 
*/  

我們可能期望能夠獲得真實的泛型參數,但是僅僅獲得了聲明時泛型參數佔位符。getTypeParameters 方法的 Javadoc 也是這麼解釋的:僅返回聲明時的泛型參數。所以,通過 getTypeParamters 方法無法獲得運行時的泛型信息。

擦除到上限

class A<T extends Number> {
}

再用 javap -v cc.unmi.A 來看泛型簽名
Signature: #18 // <T:Ljava/lang/Number;>Ljava/lang/Object;

轉換

本來a是帶有泛型信息,但是b沒有,所以在賦值過程中泛型信息就丟失了,b中的T的類型會變成其上限Number

//一下代碼中,當把一個帶泛型信息的a的實例賦值給一個不帶泛型信息的的b的時候,
//a中所有的泛型信息都會發生丟失,也就是說T是Integer的這一信息會丟失,b只知道
//T的類型是Number而已
package ErasureAndConversion;
 
 
import UseIt.A1;
 
class Apple<T extends  Number>{
    T size;
    public Apple(){
 
    }
    public Apple(T size){
        this.size = size;
    }
    public void setSize(T size){
        this.size = size;
    }
    public T getSize(){
        return this.size;
    }
}
 
 
public class Erasure {
    public static void main(String args[]){
        Apple<Integer> a = new Apple<>(6);
//        a指向的實例是帶有泛型信息的
 
        Integer as = a.getSize();
//        實例a中的T是Integer類型的,所以賦值給as沒有任何問題
        Apple b  = a;
//        這一句就是說明問題的關鍵了,b是不帶泛型信息的,所以a中的泛型信息
//        也就會被擦除,所以,a中的T的類型就只是Number而已了
 
        Number size1 = b.getSize();
//        b中的T類型是Number,所以賦值給Number類型的值沒有任何問題
 
 
//        Integer size2 = b.getSize();
//        但是,b中的T並不是Integer的了,因爲泛型信息已經被擦除了,所以這一句會
//        報錯。
        Integer size3 = a.getSize();
//       最後這一句並不會報錯,b會發生泛型信息丟失但是a並不會受影響
    }
}

下面這兩個例子說明的是一樣的問題,或者說第二個例子是第一個例子的直觀表現,說的是當把帶有泛型信息的集合賦值給沒有泛型信息的集合時泛型信息就丟失了,所以在把list賦值給ls的時候不會發生問題,因爲list已經不知道具體的泛型信息是什麼了,所以是Object,所以可以賦值給ls,但是一旦要訪問集合中的元素的時候,就會發生類型不匹配的問題。

//java允許把一個List賦值給一個List<Type>所以在下面 List<String> ls = list;
//這一句僅僅只會發生警告而已。
package ErasureAndConversion;
 
import java.util.ArrayList;
import java.util.List;
 
public class Erasure2 {
    public static void main(String args[]){
        List<Integer> li = new ArrayList<>();
        li.add(6);
        li.add(5);
        List list = li;
        List<String> ls = list;
//        一樣的道理,List沒有泛型信息,所以li的泛型信息就丟失了,所以賦值給ls
//        是沒有問題的
 
//        System.out.println(ls.get(0));
//        但是當訪問ls中的元素的時候,就會發生類型不匹配的問題
    }
}
//這個例子和上面的例子是一模一樣的
package ErasureAndConversion;
 
import java.util.ArrayList;
import java.util.List;
 
public class Erasure3 {
    public static void main(String args[]){
        List list = new ArrayList();
        ((ArrayList) list).add(5);
        ((ArrayList) list).add(4);
//        System.out.println((String)list.get(0));
    }
}

泛型信息的獲取

繼承一個泛型基類

class A<T, ID> {  
}  
  
class B extends A<String, Integer> {  
}  
  
public class Generic {  
    public static void main(String[] args) {  
        ParameterizedType parameterizedType = (ParameterizedType) B.class.getGenericSuperclass();  
        
		Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();  
		for(Type actualTypeArgument: actualTypeArguments) {  
		    System.out.println(actualTypeArgument);  
		}  
		
		Class clazz = (Class) parameterizedType.getActualTypeArguments()[0];
        System.out.println(clazz);
    }  
}  

上面的代碼輸出:
class java.lang.String
class java.lang.Integer
class java.lang.String

實現一個泛型接口

interface A<T, ID> {  
}  
  
class B implements A<String, Integer> {  
}  

public class Generic {  
    public static void main(String[] args) {  
        ParameterizedType parameterizedType = (ParameterizedType) B.class.getGenericInterfaces()[0];  
        
		Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();  
		for (Type actualTypeArgument : actualTypeArguments) {  
		    System.out.println(actualTypeArgument);  
		}  
		
		Class clazz = (Class) parameterizedType.getActualTypeArguments()[0];
        System.out.println(clazz);
    }  
}

同樣能得到上面的一樣的結果。

運行時泛型信息的獲取 (假象,實質是通過定義類的方式)

引入一點小知識
匿名內部類
概念:即內部類的簡化寫法
前提:存在一個類(可以是具體類也可以是抽象類)或接口
格式:new 類名或接口名{重寫的方法}
本質:創建的是繼承了類或實現了接口的子類匿名對象
匿名類的聲明:
匿名類的聲明是由java編譯器自動派生自一個類實例創建表達式;
匿名類永遠不能是抽象的;
匿名類總是隱式的final;
匿名類總是一個內部類,並且不能是static的;

由於Java泛型的實現機制,使用了泛型的代碼在運行期間相關的泛型參數的類型會被擦除,我們無法在運行期間獲知泛型參數的具體類型(所有的泛型類型在運行時都是Object類型)。
Java 引入泛型擦除的原因是避免因爲引入泛型而導致運行時創建不必要的類。那我們其實就可以通過定義類的方式,在類信息中保留泛型信息,從而在運行時獲得這些泛型信息。簡而言之,Java 的泛型擦除是有範圍的,即類定義中的泛型是不會被擦除的。

有些場景中,我們需要獲取泛型信息的。比如,在調用 HTTP 或 RPC 接口時,我們需要進行序列化和反序列的工作。
例如,我們通過一個 HTTP 接口接收如下的 JSON 數據

[{  
    "name": "Stark",  
    "nickName": "Iron Man"  
},  
{  
    "name": "Rogers",  
    "nickName": "Captain America"  
}]  

我們需要將其映射爲 List<Avenger>。

但是之前我們提到了泛型擦除,那我們所使用的 HTTP 或 RPC 框架是如何獲取 List 中的泛型信息呢?

如下代碼

Map<String, Integer> map = new HashMap<String, Integer>() {};  
Type type = map.getClass().getGenericSuperclass();  
ParameterizedType parameterizedType = ParameterizedType.class.cast(type);  
for (Type typeArgument : parameterizedType.getActualTypeArguments()) {  
    System.out.println(typeArgument.getTypeName());  
}  
  
/* Output 
java.lang.String 
java.lang.Integer 
*/  

上面這段代碼展示瞭如何獲取 map 這個實例所對應類的泛型信息。顯然,這次我們成功獲得了其中泛型參數信息。有了這些泛型參數,上面所提到的序列化和反序列化工作就是可能的了。

那爲什麼之前不可以,而這次可以了呢?請注意一個細節

前面的變量聲明

Map<Integer, String> map = new HashMap<Integer, String>();  

本節中的變量聲明

Map<String, Integer> map = new HashMap<String, Integer>() {};  

其中最關鍵的差別是本節的變量聲明多了一對大括號,其實是創建了一個匿名內部類,這個類是 HashMap 的子類,泛型參數限定爲了 String 和 Integer,這樣就通過定義類的方式保留了泛型信息。

框架中的應用

其實很多框架就是使用類定義中的泛型不會被擦除這個特性,實現了相應的功能。

例如,Spring Web 模塊的 RestTemplate,我們可以使用如下寫法:

ResponseEntity<YourType> responseEntity = 
restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<YourType>() {});   

其中的 new ParameterizedTypeReference<YourType>() {} 就是通過定義一個匿名內部類的方式來獲得泛型信息,從而進行反序列化的工作。

總結

Java 泛型擦除是 Java 泛型中的一個重要特性,其目的是避免過多的創建類而造成的運行時的過度消耗。所以,想 ArrayList<Integer> 和 ArrayList<String> 這兩個實例,其類實例是同一個。

但很多情況下我們又需要在運行時獲得泛型信息,那我們可以通過定義類的方式(通常爲匿名內部類,因爲我們創建這個類只是爲了獲得泛型信息)在運行時獲得泛型參數,從而滿足例如序列化、反序列化等工作的需要。

只要理解了 Java 引入泛型擦除的原因,也自然能理解如何在運行時獲取泛型信息了。

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