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 之所以要避免在創(chuàng)建泛型實例時而創(chuàng)建新的類,從而避免運行時的過度消耗。
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] */
我們可能期望能夠獲得真實的泛型參數(shù),但是僅僅獲得了聲明時泛型參數(shù)占位符。getTypeParameters 方法的 Javadoc 也是這么解釋的:僅返回聲明時的泛型參數(shù)。所以,通過 getTypeParamters 方法無法獲得運行時的泛型信息。
擦除到上限
class A<T extends Number> { }
再用 javap -v cc.unmi.A 來看泛型簽名
Signature: #18 // <T:Ljava/lang/Number;>Ljava/lang/Object;
轉(zhuǎn)換
本來a是帶有泛型信息,但是b沒有,所以在賦值過程中泛型信息就丟失了,b中的T的類型會變成其上限Number
//一下代碼中,當(dāng)把一個帶泛型信息的a的實例賦值給一個不帶泛型信息的的b的時候, //a中所有的泛型信息都會發(fā)生丟失,也就是說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; // 這一句就是說明問題的關(guān)鍵了,b是不帶泛型信息的,所以a中的泛型信息 // 也就會被擦除,所以,a中的T的類型就只是Number而已了 Number size1 = b.getSize(); // b中的T類型是Number,所以賦值給Number類型的值沒有任何問題 // Integer size2 = b.getSize(); // 但是,b中的T并不是Integer的了,因為泛型信息已經(jīng)被擦除了,所以這一句會 // 報錯。 Integer size3 = a.getSize(); // 最后這一句并不會報錯,b會發(fā)生泛型信息丟失但是a并不會受影響 } }
下面這兩個例子說明的是一樣的問題,或者說第二個例子是第一個例子的直觀表現(xiàn),說的是當(dāng)把帶有泛型信息的集合賦值給沒有泛型信息的集合時泛型信息就丟失了,所以在把list賦值給ls的時候不會發(fā)生問題,因為list已經(jīng)不知道具體的泛型信息是什么了,所以是Object,所以可以賦值給ls,但是一旦要訪問集合中的元素的時候,就會發(fā)生類型不匹配的問題。
//java允許把一個List賦值給一個List<Type>所以在下面 List<String> ls = list; //這一句僅僅只會發(fā)生警告而已。 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)); // 但是當(dāng)訪問ls中的元素的時候,就會發(fā)生類型不匹配的問題 } }
//這個例子和上面的例子是一模一樣的 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
實現(xiàn)一個泛型接口
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(); Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); for (Type actualTypeArgument : actualTypeArguments) { System.out.println(actualTypeArgument); } Class clazz = (Class) parameterizedType.getActualTypeArguments()[0]; System.out.println(clazz); } }
同樣能得到上面的一樣的結(jié)果。
運行時泛型信息的獲取 (假象,實質(zhì)是通過定義類的方式)
引入一點小知識
匿名內(nèi)部類:
- 概念:即內(nèi)部類的簡化寫法
- 前提:存在一個類(可以是具體類也可以是抽象類)或接口
- 格式:new 類名或接口名{重寫的方法}
- 本質(zhì):創(chuàng)建的是繼承了類或?qū)崿F(xiàn)了接口的子類匿名對象
匿名類的聲明:
- 匿名類的聲明是由java編譯器自動派生自一個類實例創(chuàng)建表達式;
- 匿名類永遠不能是抽象的;
- 匿名類總是隱式的final;
- 匿名類總是一個內(nèi)部類,并且不能是static的;
由于Java泛型的實現(xiàn)機制,使用了泛型的代碼在運行期間相關(guān)的泛型參數(shù)的類型會被擦除,我們無法在運行期間獲知泛型參數(shù)的具體類型(所有的泛型類型在運行時都是Object類型),但是在編譯java源代碼成 class文件中還是保存了泛型相關(guān)的信息,,這些信息被保存在class字節(jié)碼常量池中,使用了泛型的代碼處會生成一個signature簽名字段,通過簽名signature字段指明這個常量池的地址。
Java 引入泛型擦除的原因是避免因為引入泛型而導(dǎo)致運行時創(chuàng)建不必要的類。通過前面的知識我們其實就可以通過定義類的方式,在類信息中保留泛型信息,從而獲得這些泛型信息。簡而言之,Java 的泛型擦除是有范圍的,即類定義中的泛型是不會被擦除的。
有些場景中,我們需要獲取泛型信息的。比如,在調(diào)用 HTTP 或 RPC 接口時,我們需要進行序列化和反序列的工作。
例如,我們通過一個 HTTP 接口接收如下的 JSON 數(shù)據(jù)
[{ "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); //ParameterizedType parameterizedType = (ParameterizedType)map.getClass().getGenericSuperclass(); for (Type typeArgument : parameterizedType.getActualTypeArguments()) { System.out.println(typeArgument.getTypeName()); } /* Output java.lang.String java.lang.Integer */
上面這段代碼展示了如何獲取 map 這個實例所對應(yīng)類的泛型信息。顯然,這次我們成功獲得了其中泛型參數(shù)信息。有了這些泛型參數(shù),上面所提到的序列化和反序列化工作就是可能的了。
那為什么之前不可以,而這次可以了呢?請注意一個細節(jié)
前面的變量聲明
Map<Integer, String> map = new HashMap<Integer, String>();
本節(jié)中的變量聲明
Map<String, Integer> map = new HashMap<String, Integer>() {};
其中最關(guān)鍵的差別是本節(jié)的變量聲明多了一對大括號,其實是創(chuàng)建了一個匿名內(nèi)部類,這個類是 HashMap 的子類,泛型參數(shù)限定為了 String 和 Integer,這樣就通過定義類的方式保留了泛型信息。
框架中的應(yīng)用
其實很多框架就是使用類定義中的泛型不會被擦除這個特性,實現(xiàn)了相應(yīng)的功能。
例如,SpringWeb模塊的RestTemplate 和 alibaba的fastJson,我們可以使用如下寫法:
//這里的ParameterizedTypeReference是一個抽象類,因此約束了必須創(chuàng)建ParameterizedTypeReference的子類,由此成功獲取到泛型的實際類型 ResponseEntity<ResponseDTO<UserKeyDTO>> result = restTemplate.exchange(url, null, new ParameterizedTypeReference<ResponseDTO<UserKeyDTO>>(){}); //通過創(chuàng)建TypeReference的匿名內(nèi)部類的方式來保留反省信息,以便json反序列化時能反射獲取到泛型實際類型 ResponseDTO<SysCryptDTO> responseDTO = JSONObject.parseObject(jsonString, new TypeReference<ResponseDTO<SysCryptDTO>>() {});
其中的 new ParameterizedTypeReference<YourType>() {} 就是通過定義一個匿名內(nèi)部類的方式來獲得泛型信息,從而進行反序列化的工作。
總結(jié)
Java 泛型擦除是 Java 泛型中的一個重要特性,其目的是避免過多的創(chuàng)建類而造成的運行時的過度消耗。所以,想 ArrayList<Integer> 和 ArrayList<String> 這兩個實例,其類實例是同一個。
但很多情況下我們又需要在運行時獲得泛型信息,那我們可以通過定義類的方式(通常為匿名內(nèi)部類,因為我們創(chuàng)建這個類只是為了獲得泛型信息)在運行時獲得泛型參數(shù),從而滿足例如序列化、反序列化等工作的需要。
只要理解了 Java 引入泛型擦除的原因,也自然能理解如何在運行時獲取泛型信息了。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring Boot jar可執(zhí)行原理的徹底分析
這篇文章主要給大家介紹了關(guān)于Spring Boot jar可執(zhí)行原理的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07詳解Intellij IDEA 2017 debug斷點調(diào)試技巧(總結(jié))
這篇文章主要介紹了詳解Intellij IDEA 2017 debug斷點調(diào)試技巧(總結(jié)),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-11-11Java實現(xiàn)統(tǒng)計在線人數(shù)功能的方法詳解
很多人在筆試或者面試中問到:現(xiàn)在要你實現(xiàn)一個統(tǒng)計在線人數(shù)的功能,你該怎么設(shè)計?不知道的朋友,這篇文章就來告訴你具體實現(xiàn)方法2022-08-08java聯(lián)系人管理系統(tǒng)簡單設(shè)計
這篇文章主要為大家詳細介紹了java聯(lián)系人管理系統(tǒng)簡單設(shè)計,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10