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