Java中泛型學(xué)習(xí)之細(xì)節(jié)篇
簡(jiǎn)介
泛型的作用就是把類型參數(shù)化,也就是我們常說的類型參數(shù)
平時(shí)我們接觸的普通方法的參數(shù),比如public void fun(String s);參數(shù)的類型是String,是固定的
現(xiàn)在泛型的作用就是再將String定義為可變的參數(shù),即定義一個(gè)類型參數(shù)T,比如public static <T> void fun(T t);
這時(shí)參數(shù)的類型就是T的類型,是不固定的
從上面的String和T來看,泛型有著濃濃的多態(tài)的味道,但實(shí)際上泛型跟多態(tài)還是有區(qū)別的
從本質(zhì)上來講,多態(tài)是Java中的一個(gè)特性,一個(gè)概念,泛型是真實(shí)存在的一種類型;
正文中大部分示例都是以集合中的泛型為例來做介紹,因?yàn)橛玫谋容^多,大家都熟悉
正文
什么是類型參數(shù)
類型參數(shù)就是參數(shù)的類型,它接受類作為實(shí)際的值
白話一點(diǎn)來說,就是你可以把類型參數(shù)看作形參,把實(shí)際傳入的類看作實(shí)參
比如:ArrayList<E>中的類型參數(shù)E看做形參, ArrayList<String>中的類String看做實(shí)參
如果你學(xué)過工廠設(shè)計(jì)模式,那么就可以把這里的ArrayList<E>看做一個(gè)工廠類,然后你需要什么類型的ArrayList,就傳入對(duì)應(yīng)的類型參數(shù)即可
比如,傳入Integer則為ArrayList<Integer>類
比如,傳入String則為ArrayList<String>類
為啥要有泛型
主要是為了提高代碼可讀性和安全性
具體的要從泛型的演變史說起
泛型的演變史
從廣義上來說,泛型很早就有了,只是隱式存在的;
比如List list = new ArrayList(); //等價(jià)于List<Object> list = new ArrayList<>();
但是這個(gè)時(shí)候的泛型是很脆弱的,可讀性和安全性都很差(這個(gè)時(shí)期的集合相對(duì)于數(shù)組來說,優(yōu)勢(shì)還不是很大)
首先,填充數(shù)據(jù)時(shí),沒有類型檢查,那就有可能把Cat放到Dog集合中
其次,取出時(shí),需要類型轉(zhuǎn)換,如果你很幸運(yùn)的把對(duì)象放錯(cuò)了集合(有可能是故意的),那么運(yùn)行時(shí)就會(huì)報(bào)錯(cuò)轉(zhuǎn)換異常(但是編譯卻可以通過)
不過到了JDK1.5,出現(xiàn)了真正意義上的泛型(類型參數(shù),用尖括號(hào)<>表示);
比如List<E>
集合類,其中的E就是泛型的類型參數(shù),因?yàn)榧现卸际谴娴脑谽lement,所以用E字母替代(類似還有T,S,K-key,V-value);
這個(gè)時(shí)候,程序的健壯性就提高了,可讀性和安全性也都很高,看一眼就知道放進(jìn)去的是個(gè)啥東西(這個(gè)時(shí)期的集合相對(duì)于數(shù)組來說,優(yōu)勢(shì)就很明顯了)
現(xiàn)在拿List<Dog> list = new ArrayList<>()
;來舉例說明
首先,填充數(shù)據(jù)時(shí),編譯器自己會(huì)進(jìn)行類型檢查,防止將Cat放入Dog中
其次,取出數(shù)據(jù)時(shí),不需要我們手動(dòng)進(jìn)行類型轉(zhuǎn)換,編譯器自己會(huì)進(jìn)行類型轉(zhuǎn)換
細(xì)心的你可能發(fā)現(xiàn)了,既然有了泛型,那我放進(jìn)去的是Dog,取出的不應(yīng)該也是Dog嗎?為啥編譯器還要類型轉(zhuǎn)換呢?
這里就引出類型擦除的概念
類型擦除
什么是類型擦除?
類型擦除指的是,你在給類型參數(shù)<T>賦值時(shí),編譯器會(huì)將實(shí)參類型擦除為Object(這里假設(shè)沒有限定符,限定符下面會(huì)講到)
所以這里我們要明白一個(gè)東西:虛擬機(jī)中沒有泛型類型對(duì)象的概念,在它眼里所有對(duì)象都是普通對(duì)象
比如下面的代碼
擦除前
public?class?EraseDemo<T>?{ ? ?private?T?t; ? ?public?static?void?main(String[]?args) { ? ? ? ? ? } ? ?public?T?getT(){ ? ? ? ?return?t; ? } ? ?public?void?setT(T?t){ ? ? ? ?this.t?=?t; ? } }
擦除后
public?class?EraseDemo?{ ? ?private?Object?t; ? ?public?static?void?main(String[]?args) { ? ? ? ? ? } ? ?public?Object?getT(){ ? ? ? ?return?t; ? } ? ?public?void?setT(Object?t){ ? ? ? ?this.t?=?t; ? } }
可以看到,T都變成了Object
泛型類被擦除后的類型,我們一般叫它原始類型(raw type),比如EraseDemo<T>擦除后的原始類型就是EraseDemo
相應(yīng)的,如果你有兩個(gè)數(shù)組列表,ArrayList<String>和ArrayList<Integer> ,編譯器也會(huì)把兩者都擦除為ArrayList
你可以通過代碼來測(cè)試一下
ArrayList<String>?list1?=?new?ArrayList<>(); ArrayList<Integer>?list2?=?new?ArrayList<>(); System.out.println(list1.getClass()?==?list2.getClass());// 這里會(huì)打印true
上面提到的限定符是干嘛的?
限定符就是用來限定邊界的,如果泛型有設(shè)置邊界,比如<T extends Animal>
,那么擦除時(shí),會(huì)擦除到第一個(gè)邊界Animal類,而不是Object類
下面還是以上面的代碼為例,展示下擦除前后的對(duì)比
擦除前:
public?class?EraseDemo<T?extends?Animal>?{ ? ?private?T?t; ? ?public?static?void?main(String[]?args) { ? ? ? ? ? } ? ?public?T?getT(){ ? ? ? ?return?t; ? } ? ?public?void?setT(T?t){ ? ? ? ?this.t?=?t; ? } }
擦除后:
public?class?EraseDemo?{ ? ?private?Animal?t; ? ?public?static?void?main(String[]?args) { ? ? ? ? ? } ? ?public?Animal?getT(){ ? ? ? ?return?t; ? } ? ?public?void?setT(Animal?t){ ? ? ? ?this.t?=?t; ? } }
這里的extends符號(hào)是表示繼承的意思嗎?
不是的,這里的extends只是表示前者是后者的一個(gè)子類,可以繼承也可以實(shí)現(xiàn)
之所以用extends只是因?yàn)檫@個(gè)關(guān)鍵詞已經(jīng)內(nèi)置在Java中了,且比較符合情景
如果自己再造一個(gè)關(guān)鍵詞,比如sub,可能會(huì)使得某些舊代碼產(chǎn)生問題(比如使用sub作為變量的代碼)
為什么要擦除呢?
這其實(shí)不是想不想擦除的問題,而是不得不擦除的問題
因?yàn)榕f代碼是沒有泛型概念的,這里的擦除主要是為了兼容舊代碼,使得舊代碼和新代碼可以互相調(diào)用
泛型的應(yīng)用場(chǎng)景
從大的方向來說:
用在類中:叫做泛型類,類名后面緊跟<類型參數(shù)>,比如ArrayList<E>
用在方法中:叫做泛型方法,方法的返回值前面添加<類型參數(shù)>,比如:public <T> void fun(T obj)
是不是想到了抽象類和抽象方法?
還是有區(qū)別的,抽象類和抽象方法是相互關(guān)聯(lián)的,但是泛型類和泛型方法之間沒有聯(lián)系
集中到類的方向來說:泛型多用在集合類中,比如ArrayList<E>
如果是自定義泛型的話,推薦用泛型方法,原因有二:
脫離泛型類單獨(dú)使用,使代碼更加清晰(不用為了某個(gè)小功能而泛化整個(gè)類)
泛型類中,靜態(tài)方法無法使用類型參數(shù);但是靜態(tài)的泛型方法可以
通配符限定
這里主要介紹<T>, <? extends T>, <? super T>的區(qū)別
<T>:這個(gè)是最常用的,就是普通的類型參數(shù),在調(diào)用時(shí)傳入實(shí)際的類來替換T即可,這個(gè)實(shí)際的類可以是T,也可以是T的子類
比如List<String> list = new ArrayList<>();,這里的String就是實(shí)際傳入的類,用來替換類型參數(shù)T
<? extends T>:這個(gè)屬于通配符限定中的子類型限定,即傳入實(shí)際的類必須是T或者T子類
乍一看,這個(gè)有點(diǎn)像<T>類型參數(shù),都是往里放T或者T的子類;
但是區(qū)別還是挺多的,后面會(huì)列出
<? super T>:這個(gè)屬于通配符限定中的超類型限定,即傳入實(shí)際的類必須是T或者T的父類
<?>:這個(gè)屬于無限定通配符,即它也不知道里面該放啥類型,所以干脆就不讓你往里添加,只能獲?。ㄟ@一點(diǎn)類似<? extends T>)
下面用表格列出<T>,<? extends T>, <? super T>的幾個(gè)比較明細(xì)的區(qū)別
<T> | <? extends T> | <? super T> | |
---|---|---|---|
類型擦除 | 傳入實(shí)參時(shí),實(shí)參被擦為Object,但是在get時(shí)編譯器會(huì)自動(dòng)轉(zhuǎn)為T | 擦到T | 擦到Object |
引用對(duì)象 | 不能將引用指向子類型或者父類型的對(duì)象,比如:List<Animal> list = new ArrayList<Cat>();//報(bào)錯(cuò) | 能將引用指向子類型的對(duì)象,比如:List<? extends Animal> list = new ArrayList<Cat>(); | 能將引用指向父類型的對(duì)象,比如:List<? super Cat> list = new ArrayList<Animal>(); |
添加數(shù)據(jù) | 可以添加數(shù)據(jù),T或者T的子類 | 不能 | 能,T或者T的子類 |
下面我們用代碼來演示下
類型擦除:
// <T>類型,傳入實(shí)參時(shí),擦除為Object,但是get時(shí)還是實(shí)參的類型 List<Animal>?list1?=?new?ArrayList<>();// 合法 list1.add(new?Dog());// 合法 Animal?animal?=?list1.get(0);?// 這里不需要強(qiáng)轉(zhuǎn),雖然前面?zhèn)魅雽?shí)參時(shí)被擦除為Object,但是get時(shí)編譯器內(nèi)部已經(jīng)做了強(qiáng)制類型轉(zhuǎn)換 // <? extends T> 子類型的通配符限定,擦除到T(整個(gè)過程不再變) List<??extends?Animal>?list2?=?list1;// 合法 Animal?animal2?=?list2.get(0);?// 這里不需要強(qiáng)轉(zhuǎn),因?yàn)橹徊脸絋(即Animal) // <? super T> 超類型的通配符限定,擦除到Object List<??super?Animal>?list3?=?list1;?// 合法 Animal?animal3?=?(Animal)list3.get(0);?// 需要手動(dòng)強(qiáng)制,因?yàn)楸徊脸絆bject
將引用指向子類型或父類型的對(duì)象:
// <T>類型,不能指向子類型或父類型 List<Animal>?list?=?new?ArrayList<Dog>();// 報(bào)錯(cuò):需要的是List<Animal>,提供的是ArrayList<Dog> // <? extends T> 子類型的通配符限定,指向子類型 List<??extends?Animal>?list2?=?new?ArrayList<Dog>();// 合法 // <? super T> 超類型的通配符限定,指向父類型 List<??super?Dog>?list3?=?new?ArrayList<Animal>();?// 合法
添加數(shù)據(jù)
// <T>類型,可以添加T或者T的子類型 List<Animal>?list1?=?new?ArrayList<>(); list.add(new?Dog());// 合法 // <? extends T> 子類型的通配符限定,不能添加元素 List<??extends?Animal>?list2?=?new?ArrayList<Dog>();// 正確 list2.add(new?Dog());?// 報(bào)錯(cuò):不能往里添加元素 // <? super T> 超類型的通配符限定,可以添加T或者T的子類型 List<??super?Dog>?list3?=?new?ArrayList<Animal>(); list3.add(new?Dog());?// 合法,可以添加T類型的元素 list3.add(new?Animal());//報(bào)錯(cuò),不能添加父類型的元素
下面針對(duì)上面的測(cè)試結(jié)果進(jìn)行解惑
先從<T>的報(bào)錯(cuò)開始吧
為啥<T>類型的引用不能指向子類型,比如 List<Animal> list = new ArrayList<Dog>();
首先說明一點(diǎn),Animal和Dog雖然是父子關(guān)系(Dog繼承Animal),但是List<Animal> 和 List<Dog>之間是沒有任何關(guān)系的(有點(diǎn)像Java和Javascript)
他們之間的關(guān)系如下圖
之所以這樣設(shè)計(jì),主要是為了類型安全的考慮
下面用代碼演示,假設(shè)可以將List<Animal>指向子類List<Dog>
List<Animal>?list?=?new?ArrayList<Dog>();// 假設(shè)這里不報(bào)錯(cuò) list.add(new?Cat());?//這里把貓放到狗里面了
第二行可以看到,很明顯,把貓放到狗里面是不對(duì)的,這就又回到了泛型真正出現(xiàn)之前的時(shí)期了(沒有泛型,集合存取數(shù)據(jù)時(shí)不安全)
那為啥<? extends T>就能指向子類型呢?比如List<? extends Animal> list = new ArrayList<Dog>();
說的淺一點(diǎn),原因是:這個(gè)通配符限定出現(xiàn)的目的就是為了解決上面的不能指向子類的問題
當(dāng)然,這個(gè)原因說了跟沒說一樣。下面開始正經(jīng)點(diǎn)解釋吧
因?yàn)檫@個(gè)通配符限定不允許插入任何數(shù)據(jù),所以當(dāng)你指向子類型時(shí),這個(gè)list就只能存放指向的那個(gè)集合里的數(shù)據(jù)了,而不能再往里添加;
自然的也就類型安全了,只能訪問,不能添加
為什么<? extends T>不允許插入數(shù)據(jù)呢?
其實(shí)這個(gè)的原因跟上面的修改引用對(duì)象是相輔相成的,合起來就是為了保證泛型的類型安全性
考慮下面的代碼
List<Animal>?list?=?new?ArrayList<>(); list.add(new?Cat()); list.add(new?Dog()); Dog?d?=?(Dog)?list.get(0);?// 報(bào)錯(cuò),轉(zhuǎn)換異常
可以看到,插入的子類很混亂,導(dǎo)致提取時(shí)轉(zhuǎn)型容易出錯(cuò)(這是泛型<T>的一個(gè)弊端,當(dāng)然我們寫的時(shí)候多用點(diǎn)心可能就不會(huì)這個(gè)問題)
但是有了<? extends T>之后,就不一樣了
首先你可以通過修改引用的對(duì)象來使得list指向不同的Animal子類
其次你添加數(shù)據(jù),不能直接添加,但是可以通過指向的Animal子類對(duì)象來添加
這樣就保證了類型的安全性
代碼如下:
// 定義一個(gè)Dog集合 List<Dog>?listDog?=?new?ArrayList<>(); listDog.add(new?Dog()); // 讓<? extends Animal>通配符限定的泛型 指向上面的Dog集合 List<??extends?Animal>?list2?=?listDog; // 這時(shí)如果想往里添加數(shù)據(jù),只需要操作listDog即可,它可以保證類型安全 listDog.add(new?Dog()); // 如果自己去添加,就會(huì)報(bào)錯(cuò) list2.add(new?Dog());// 報(bào)錯(cuò)
<? extends T>一般用在形參,這樣我們需要哪個(gè)子類型,只需要傳入對(duì)應(yīng)子類的泛型對(duì)象就可以了,從而實(shí)現(xiàn)泛型中的多態(tài)
<? super T>為啥可以插入呢?
兩個(gè)原因
它只能插入T或者T的子類
它的下限是T
也就是說你隨便插入,我已經(jīng)限制了你插入的類型為T或者T的子類
那么我在查詢時(shí),就可以放心的轉(zhuǎn)為T或者T的父類
代碼如下:
List<??super?Dog>?listDog?=?new?ArrayList<>(); listDog.add(new?Dog()); listDog.add(new?Cat());?// 報(bào)錯(cuò) listDog.add(new?Animal());?// 報(bào)錯(cuò) Dog?dog?=?(Dog)?listDog.get(0);?// 內(nèi)部被擦除為Object,所以要手動(dòng)強(qiáng)轉(zhuǎn)
為啥<T>獲取時(shí),編譯器會(huì)自動(dòng)強(qiáng)轉(zhuǎn)轉(zhuǎn)換,到了這里<? super T>,就要手動(dòng)轉(zhuǎn)換了呢?
這個(gè)可能是因?yàn)榫幾g器也不確定你的要返回的T的父類是什么類型,所以干脆留給你自己來處理了
但是如果你把這個(gè)listDog指向一個(gè)父類的泛型對(duì)象,然后又在父類的泛型對(duì)象中,插入其他類型,那可就亂了(又回到<T>的問題了,要自己多注意)
比如:
List<Animal>?list?=?new?ArrayList<>(); list.add(new?Cat());?// 加了Cat // 指向Animal List<??super?Dog>?listDog?=?list; listDog.add(new?Dog()); list.add(new?Cat());?// 報(bào)錯(cuò) list.add(new?Animal());?// 報(bào)錯(cuò) Dog?dog?=?(Dog)?listDog.get(0);?//報(bào)錯(cuò):轉(zhuǎn)換異常Cat-》Dog
所以建議<? super T>在添加數(shù)據(jù)的時(shí)候,盡量集中在一個(gè)地方,不要多個(gè)地方添加,像上面的,要么都在<? super Dog>里添加數(shù)據(jù),要么都在<Animal>中添加
動(dòng)態(tài)類型安全檢查
這個(gè)主要是為了跟舊代碼兼容,對(duì)舊代碼進(jìn)行的一種類型安全檢查,防止將Cat插入Dog集合中這種錯(cuò)誤
這種檢查是發(fā)生在編譯階段,這樣就可以提早發(fā)現(xiàn)問題
對(duì)應(yīng)的類為Collections工具類,方法如下圖
代碼如下
// 動(dòng)態(tài)類型安全檢查,在與舊代碼兼容時(shí),防止將Dog放到Cat集合中類似的問題 // === 檢查之前 === List?list?=?new?ArrayList<Integer>(); // 添加不報(bào)錯(cuò) list.add("a"); list.add(1); // 只有用的時(shí)候,才會(huì)報(bào)錯(cuò) Integer?i?=?(Integer)?list.get(0);?// 這里運(yùn)行時(shí)報(bào)錯(cuò) // === 檢查之后 === List?list2?=?Collections.checkedList(new?ArrayList<>(),?Integer.class); // 插入時(shí)就會(huì)報(bào)錯(cuò) list2.add("a");?// 這里編譯時(shí)就報(bào)錯(cuò),提前發(fā)現(xiàn)錯(cuò)誤 list2.add(1);
總結(jié)
泛型的作用:
提高類型安全性:預(yù)防各種類型轉(zhuǎn)換問題
增加程序可讀性:所見即所得,看得到放進(jìn)去的是啥,也知道會(huì)取出啥
提高代碼重用性:多種同類型的數(shù)據(jù)(比如Animal下的Dog,Cat)可以集合到一處來處理,從而調(diào)高代碼重用性
類型擦除:
泛型T在傳入實(shí)參時(shí),實(shí)參的類型會(huì)被擦除為限定類型(即<? extends T>中的T),如果沒有限定類型,則默認(rèn)為Object
通配符限定:
<? extends T>:子類型的通配符限定,以查詢?yōu)橹?/strong>,比如消費(fèi)者集合場(chǎng)景
<? super T>:超類型的通配符限定,以添加為主,比如生產(chǎn)者集合場(chǎng)景
到此這篇關(guān)于Java中泛型學(xué)習(xí)之細(xì)節(jié)篇的文章就介紹到這了,更多相關(guān)Java中泛型細(xì)節(jié)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mybatis的xml中使用if/else標(biāo)簽的具體使用
本文主要介紹了Mybatis的xml中使用if/else標(biāo)簽的具體使用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05Java實(shí)現(xiàn)經(jīng)典大富翁游戲的示例詳解
大富翁,又名地產(chǎn)大亨。是一種多人策略圖版游戲。參與者分得游戲金錢,憑運(yùn)氣(擲骰子)及交易策略,買地、建樓以賺取租金。本文將通過Java實(shí)現(xiàn)這一經(jīng)典游戲,感興趣的可以跟隨小編一起學(xué)習(xí)一下2022-02-02MyBatis-Plus實(shí)現(xiàn)邏輯刪除功能解析
這篇文章主要介紹了MyBatis-Plus實(shí)現(xiàn)邏輯刪除功能解析,有時(shí)候并不需要真正的刪除數(shù)據(jù),而是想邏輯刪除,方便數(shù)據(jù)恢復(fù),MyBatis-Plus可以很方便的實(shí)現(xiàn)邏輯刪除的功能,需要的朋友可以參考下2023-11-11BeanUtils.copyProperties()拷貝id屬性失敗的原因及解決
這篇文章主要介紹了BeanUtils.copyProperties()拷貝id屬性失敗的原因及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09SpringBoot圖文并茂講解Lombok庫(kù)的安裝與使用
Lombok想要解決了的是在我們實(shí)體Bean中大量的Getter/Setter方法,以及toString, hashCode等可能不會(huì)用到,但是某些時(shí)候仍然需要復(fù)寫,以期方便使用的方法;在使用Lombok之后,將由其來自動(dòng)幫你實(shí)現(xiàn)代碼生成2022-06-06Java使用@Validated注解進(jìn)行參數(shù)驗(yàn)證的方法
這篇文章主要介紹了Java使用@Validated注解進(jìn)行參數(shù)驗(yàn)證的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08