關(guān)于泛型擦除問(wèn)題的解決--Mybatis查詢類型轉(zhuǎn)換
概念介紹
Java語(yǔ)言的泛型采用的是擦除法實(shí)現(xiàn)的偽泛型,泛型信息(類型變量、參數(shù)化類型)編譯之后通通被除掉了。使用擦除法的好處就是實(shí)現(xiàn)簡(jiǎn)單、非常容易Backport,運(yùn)行期也能夠節(jié)省一些類型所占的內(nèi)存空間。
而擦除法的壞處就是,通過(guò)這種機(jī)制實(shí)現(xiàn)的泛型遠(yuǎn)不如真泛型靈活和強(qiáng)大。Java選取這種方法是一種折中,因?yàn)镴ava最開(kāi)始的版本是不支持泛型的,為了兼容以前的庫(kù)而不得不使用擦除法。
驗(yàn)證擦除,我們編寫(xiě)下面代碼:
public class ErasedTypeEquivalence { public static void main(String[] args) { //例1 ArrayList<String> list1 = new ArrayList<String>(); list1.add("abc"); ArrayList<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass());//true //例2 ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //這樣調(diào)用 add 方法只能存儲(chǔ)整形,因?yàn)榉盒皖愋偷膶?shí)例為 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i));//會(huì)輸出1和asd } } }
在例1中,我們定義了兩個(gè)ArrayList數(shù)組,不過(guò)一個(gè)是ArrayList<String>泛型類型的,只能存儲(chǔ)字符串;一個(gè)是ArrayList<Integer>泛型類型的,只能存儲(chǔ)整數(shù),最后,我們通過(guò)list1對(duì)象和list2對(duì)象的getClass()方法獲取他們的類的信息,最后發(fā)現(xiàn)結(jié)果為true。說(shuō)明泛型類型String和Integer都被擦除掉了,只剩下原始類型。
在例2中,定義了一個(gè)ArrayList泛型類型實(shí)例化為Integer對(duì)象,如果直接調(diào)用add()方法,那么只能存儲(chǔ)整數(shù)數(shù)據(jù),不過(guò)當(dāng)我們利用反射調(diào)用add()方法的時(shí)候,卻可以存儲(chǔ)字符串,這說(shuō)明了Integer泛型實(shí)例在編譯之后被擦除掉了,只保留了原始類型。
上面兩次提到了原始類型,什么是原始類型?原始類型 就是擦除去了泛型信息,最后在字節(jié)碼中的類型變量的真正類型,無(wú)論何時(shí)定義一個(gè)泛型,相應(yīng)的原始類型都會(huì)被自動(dòng)提供,類型變量擦除,并使用其限定類型(無(wú)限定的變量用Object)替換。
問(wèn)題案例
最近在搭系統(tǒng)基礎(chǔ)代碼架構(gòu),其中就涉及到系統(tǒng)數(shù)據(jù)字典 功能,以前都是用varchar類型保存字典內(nèi)容,這次準(zhǔn)備玩點(diǎn)新花樣,準(zhǔn)備用上MySQL的JSON類型保存字典表的內(nèi)容字段。>>文章傳送門(mén)<<
實(shí)際操作之后就遇到了泛型擦除問(wèn)題,如下圖,我雖然對(duì)content字段的List指定了泛型DictContent,但是在做類型轉(zhuǎn)換時(shí),只能指定javaType=List,沒(méi)有也不能指定其泛型:
在沒(méi)有指定泛型的情況下,JacksonTypeHandler在做類型轉(zhuǎn)換后生成的集合的泛型就與預(yù)期的不一致:
原因分析
原因很簡(jiǎn)單,在resultMap中指定的JavaType是java.util.List,此處只能指定類類型,并不能指定泛型。而在對(duì)應(yīng)的類型轉(zhuǎn)換類中也沒(méi)有指定其泛型,而List<DictContent>和List<Object>的類類型是一樣的,所以在給content字段賦值時(shí)是不會(huì)報(bào)錯(cuò)的。但是一旦你需要操作List的中的元素,在取出元素時(shí),JVM就發(fā)現(xiàn)你要的類型是DictContent 而實(shí)際上是LinkedHashMap,就會(huì)拋出類型轉(zhuǎn)換異常。
通俗的講就是你準(zhǔn)備買華為手機(jī)(將JSON類型轉(zhuǎn)成List<DictContent>類型),但是買的時(shí)候沒(méi)有說(shuō)要買什么牌子的手機(jī)(在javaType中只指定了List類型,沒(méi)有也無(wú)法指定泛型),而店子里有很多牌子的手機(jī),所以店家就隨便給了你一款手機(jī)。。。
以下是Mybatis Plus中的部分源碼,可以看到在沒(méi)有指定List的泛型的情況下,通過(guò)JacksonTypeHandler處理后的元素類型并不是我們預(yù)期的類型:
下圖我們可以看到JacksonTypeHandler是BaseTypeHandler的子類,而且指定了BaseTypeHandler中的泛型是Object類型,但是上圖中的泛型卻是LinkedHashMap。
至于為什么是LinkedHashMap,我覺(jué)得是JVM指定的,如果哪位大佬比較清楚這塊的邏輯還請(qǐng)?jiān)谠u(píng)論中指點(diǎn)一下!
解決方案
既然原因搞清楚了,解決方案就呼之欲出了,有兩種方案:
- 自定義一個(gè)指定泛型的集合類替代List<T>
- 引用上文中通俗的說(shuō)法,這個(gè)方案就是在買手機(jī)的時(shí)候告訴賣家,我要買華為手機(jī)。
- 自定義一個(gè)指定泛型的TypeHandler類替代JacksonTypeHandler類
- 而這里的的通俗的說(shuō)法就是讓店家只賣華為手機(jī)。
以上兩種方案都可以實(shí)現(xiàn)我們的需求。
從工作量上來(lái)說(shuō),自定義一個(gè)List<T>顯然更少,所以我選擇了第一種方案,如圖:
8.11新增:第二種解決方式:
替換后結(jié)果如下:
至此,泛型擦除問(wèn)題解決。
總結(jié)
不得不說(shuō),玩新花樣總是會(huì)遇到各種各樣的坑,但是編程之路,不就是不斷的踩坑,不斷的改BUG,積累經(jīng)驗(yàn),打怪升級(jí)。如果不是因?yàn)樽罱媪诉@個(gè)新花樣,可能我這輩子都不會(huì)遇到泛型擦除的問(wèn)題!
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring中Bean的加載與SpringBoot的初始化流程詳解
這篇文章主要介紹了Spring中Bean的加載與SpringBoot的初始化流程詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11JetBrains IntelliJ IDEA 優(yōu)化教超詳細(xì)程
這篇文章主要介紹了JetBrains IntelliJ IDEA 優(yōu)化教超詳細(xì)程,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03Java StringBuilder和StringBuffer源碼分析
這篇文章主要針對(duì)Java中兩個(gè)常用的操作字符串的類 StringBuilder和StringBuffer進(jìn)行源碼分析,感興趣的小伙伴們可以參考一下2016-01-01java RocketMQ快速入門(mén)基礎(chǔ)知識(shí)
這篇文章主要介紹了java RocketMQ快速入門(mén)基礎(chǔ)知識(shí),所以RocketMQ是站在巨人的肩膀上(kafka),又對(duì)其進(jìn)行了優(yōu)化讓其更滿足互聯(lián)網(wǎng)公司的特點(diǎn)。它是純Java開(kāi)發(fā),具有高吞吐量、高可用性、適合大規(guī)模分布式系統(tǒng)應(yīng)用的特點(diǎn)。,需要的朋友可以參考下2019-06-06Netty分布式flush方法刷新buffer隊(duì)列源碼剖析
這篇文章主要為大家介紹了Netty分布式flush方法刷新buffer隊(duì)列源碼剖析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03SpringBoot使用Caffeine實(shí)現(xiàn)內(nèi)存緩存示例詳解
caffeine提供了四種緩存策略:分別為手動(dòng)加載、自動(dòng)加載、異步手動(dòng)加載、異步自動(dòng)加載,這篇文章主要介紹了SpringBoot使用Caffeine實(shí)現(xiàn)內(nèi)存緩存,需要的朋友可以參考下2023-06-06Java實(shí)現(xiàn)支付對(duì)接常用加密方式的示例代碼
這篇文章主要為大家詳細(xì)介紹了Java如何實(shí)現(xiàn)支付對(duì)接時(shí)常用加密方式,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Java有一點(diǎn)幫助,需要的可以參考一下2023-02-02