這篇文章主要講解了“Java中的泛型詳細(xì)介紹”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“Java中的泛型詳細(xì)介紹”吧!
信豐網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)公司!從網(wǎng)頁設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、APP開發(fā)、自適應(yīng)網(wǎng)站建設(shè)等網(wǎng)站項(xiàng)目制作,到程序開發(fā),運(yùn)營維護(hù)。創(chuàng)新互聯(lián)公司從2013年開始到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選創(chuàng)新互聯(lián)公司。
泛型的作用就是把類型參數(shù)化,也就是我們常說的類型參數(shù)
平時(shí)我們接觸的普通方法的參數(shù),比如public void fun(String s);參數(shù)的類型是String,是固定的
現(xiàn)在泛型的作用就是再將String定義為可變的參數(shù),即定義一個(gè)類型參數(shù)T,比如public static
這時(shí)參數(shù)的類型就是T的類型,是不固定的
從上面的String和T來看,泛型有著濃濃的多態(tài)的味道,但實(shí)際上泛型跟多態(tài)還是有區(qū)別的
從本質(zhì)上來講,多態(tài)是Java中的一個(gè)特性,一個(gè)概念,泛型是真實(shí)存在的一種類型;
下面我們詳細(xì)說下Java中的泛型相關(guān)的知識點(diǎn),目錄如下:
什么是類型參數(shù)
為啥要有泛型
泛型的演變史
類型擦除
泛型的應(yīng)用場景
通配符限定
動(dòng)態(tài)類型安全
等等
正文中大部分示例都是以集合中的泛型為例來做介紹,因?yàn)橛玫谋容^多,大家都熟悉
類型參數(shù)就是參數(shù)的類型,它接受類作為實(shí)際的值
白話一點(diǎn)來說,就是你可以把類型參數(shù)看作形參,把實(shí)際傳入的類看作實(shí)參
比如:ArrayList
中的類型參數(shù)E看做形參, ArrayList
中的類String看做實(shí)參
如果你學(xué)過工廠設(shè)計(jì)模式,那么就可以把這里的ArrayList
看做一個(gè)工廠類,然后你需要什么類型的ArrayList,就傳入對應(yīng)的類型參數(shù)即可
比如,傳入Integer則為ArrayList
類
比如,傳入String則為ArrayList
類
主要是為了提高代碼可讀性和安全性
具體的要從泛型的演變史說起
從廣義上來說,泛型很早就有了,只是隱式存在的;
比如List list = new ArrayList()
; //等價(jià)于List
但是這個(gè)時(shí)候的泛型是很脆弱的,可讀性和安全性都很差(這個(gè)時(shí)期的集合相對于數(shù)組來說,優(yōu)勢還不是很大)
首先,填充數(shù)據(jù)時(shí),沒有類型檢查,那就有可能把Cat
放到Dog
集合中
其次,取出時(shí),需要類型轉(zhuǎn)換,如果你很幸運(yùn)的把對象放錯(cuò)了集合(有可能是故意的),那么運(yùn)行時(shí)就會(huì)報(bào)錯(cuò)轉(zhuǎn)換異常(但是編譯卻可以通過)
不過到了JDK1.5,出現(xiàn)了真正意義上的泛型(類型參數(shù),用尖括號<>
表示);
比如List
集合類,其中的E就是泛型的類型參數(shù),因?yàn)榧现卸际谴娴脑谽lement,所以用E字母替代(類似還有T,S,K-key,V-value);
這個(gè)時(shí)候,程序的健壯性就提高了,可讀性和安全性也都很高,看一眼就知道放進(jìn)去的是個(gè)啥東西(這個(gè)時(shí)期的集合相對于數(shù)組來說,優(yōu)勢就很明顯了)
現(xiàn)在拿List
;來舉例說明
首先,填充數(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ù)
賦值時(shí),編譯器會(huì)將實(shí)參類型擦除為Object(這里假設(shè)沒有限定符,限定符下面會(huì)講到)
所以這里我們要明白一個(gè)東西:虛擬機(jī)中沒有泛型類型對象的概念,在它眼里所有對象都是普通對象
比如下面的代碼
擦除前
public class EraseDemo{ 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
擦除后的原始類型就是EraseDemo
相應(yīng)的,如果你有兩個(gè)數(shù)組列表,ArrayList
和ArrayList
,編譯器也會(huì)把兩者都擦除為ArrayList
你可以通過代碼來測試一下
ArrayListlist1 = new ArrayList<>(); ArrayList list2 = new ArrayList<>(); System.out.println(list1.getClass() == list2.getClass());// 這里會(huì)打印true
上面提到的限定符是干嘛的?
限定符就是用來限定邊界的,如果泛型有設(shè)置邊界,比如
,那么擦除時(shí),會(huì)擦除到第一個(gè)邊界Animal
類,而不是Object
類
下面還是以上面的代碼為例,展示下擦除前后的對比
擦除前:
public class EraseDemo{ 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符號是表示繼承的意思嗎?
不是的,這里的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)用
從大的方向來說:
用在類中:叫做泛型類,類名后面緊跟<類型參數(shù)>,比如ArrayList
用在方法中:叫做泛型方法,方法的返回值前面添加<類型參數(shù)>,比如:public
是不是想到了抽象類和抽象方法?
還是有區(qū)別的,抽象類和抽象方法是相互關(guān)聯(lián)的,但是泛型類和泛型方法之間沒有聯(lián)系
集中到類的方向來說:泛型多用在集合類中,比如ArrayList
如果是自定義泛型的話,推薦用泛型方法,原因有二:
脫離泛型類單獨(dú)使用,使代碼更加清晰(不用為了某個(gè)小功能而泛化整個(gè)類)
泛型類中,靜態(tài)方法無法使用類型參數(shù);但是靜態(tài)的泛型方法可以
這里主要介紹
的區(qū)別
:這個(gè)是最常用的,就是普通的類型參數(shù),在調(diào)用時(shí)傳入實(shí)際的類來替換T即可,這個(gè)實(shí)際的類可以是T,也可以是T的子類
比如
List
,這里的String就是實(shí)際傳入的類,用來替換類型參數(shù)Tlist = new ArrayList<>();
extends T>
:這個(gè)屬于通配符限定中的子類型限定,即傳入實(shí)際的類必須是T或者T子類
乍一看,這個(gè)有點(diǎn)像
類型參數(shù),都是往里放T或者T的子類;
但是區(qū)別還是挺多的,后面會(huì)列出
super T>
:這個(gè)屬于通配符限定中的超類型限定,即傳入實(shí)際的類必須是T或者T的父類
>
:這個(gè)屬于無限定通配符,即它也不知道里面該放啥類型,所以干脆就不讓你往里添加,只能獲?。ㄟ@一點(diǎn)類似 extends T>
)
下面用表格列出
, extends T>
, super T>
的幾個(gè)比較明細(xì)的區(qū)別
| extends T> | super T> | |
---|---|---|---|
類型擦除 | 傳入實(shí)參時(shí),實(shí)參被擦為Object,但是在get時(shí)編譯器會(huì)自動(dòng)轉(zhuǎn)為T | 擦到T | 擦到Object |
引用對象 | 不能將引用指向子類型或者父類型的對象,比如:List //報(bào)錯(cuò) | 能將引用指向子類型的對象,比如:List extends Animal> list = new ArrayList | 能將引用指向父類型的對象,比如:List super Cat> list = new ArrayList |
添加數(shù)據(jù) | 可以添加數(shù)據(jù),T或者T的子類 | 不能 | 能,T或者T的子類 |
下面我們用代碼來演示下
類型擦除:
//類型,傳入實(shí)參時(shí),擦除為Object,但是get時(shí)還是實(shí)參的類型 List 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
將引用指向子類型或父類型的對象:
//類型,不能指向子類型或父類型 List list = new ArrayList ();// 報(bào)錯(cuò):需要的是List ,提供的是ArrayList // extends T> 子類型的通配符限定,指向子類型 List extends Animal> list2 = new ArrayList ();// 合法 // super T> 超類型的通配符限定,指向父類型 List super Dog> list3 = new ArrayList (); // 合法
添加數(shù)據(jù)
//類型,可以添加T或者T的子類型 List list1 = new ArrayList<>(); list.add(new Dog());// 合法 // extends T> 子類型的通配符限定,不能添加元素 List extends Animal> list2 = new ArrayList ();// 正確 list2.add(new Dog()); // 報(bào)錯(cuò):不能往里添加元素 // super T> 超類型的通配符限定,可以添加T或者T的子類型 List super Dog> list3 = new ArrayList (); list3.add(new Dog()); // 合法,可以添加T類型的元素 list3.add(new Animal());//報(bào)錯(cuò),不能添加父類型的元素
下面針對上面的測試結(jié)果進(jìn)行解惑
先從
的報(bào)錯(cuò)開始吧
為啥
類型的引用不能指向子類型,比如
List
list = new ArrayList ();
首先說明一點(diǎn),Animal和Dog雖然是父子關(guān)系(Dog繼承Animal),但是List
和 List
之間是沒有任何關(guān)系的(有點(diǎn)像Java和Javascript)
他們之間的關(guān)系如下圖
之所以這樣設(shè)計(jì),主要是為了類型安全的考慮
下面用代碼演示,假設(shè)可以將List
指向子類List
Listlist = new ArrayList ();// 假設(shè)這里不報(bào)錯(cuò) list.add(new Cat()); //這里把貓放到狗里面了
第二行可以看到,很明顯,把貓放到狗里面是不對的,這就又回到了泛型真正出現(xiàn)之前的時(shí)期了(沒有泛型,集合存取數(shù)據(jù)時(shí)不安全)
那為啥
extends T>
就能指向子類型呢?比如List extends Animal> list = new ArrayList
();
說的淺一點(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è)的原因跟上面的修改引用對象是相輔相成的,合起來就是為了保證泛型的類型安全性
考慮下面的代碼
Listlist = 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ò)(這是泛型
的一個(gè)弊端,當(dāng)然我們寫的時(shí)候多用點(diǎn)心可能就不會(huì)這個(gè)問題)
但是有了 extends T>
之后,就不一樣了
首先你可以通過修改引用的對象來使得list指向不同的Animal子類
其次你添加數(shù)據(jù),不能直接添加,但是可以通過指向的Animal子類對象來添加
這樣就保證了類型的安全性
代碼如下:
// 定義一個(gè)Dog集合 ListlistDog = 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è)子類型,只需要傳入對應(yīng)子類的泛型對象就可以了,從而實(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)
為啥
獲取時(shí),編譯器會(huì)自動(dòng)強(qiáng)轉(zhuǎn)轉(zhuǎn)換,到了這里
super T>
,就要手動(dòng)轉(zhuǎn)換了呢?
這個(gè)可能是因?yàn)榫幾g器也不確定你的要返回的T的父類是什么類型,所以干脆留給你自己來處理了
但是如果你把這個(gè)listDog指向一個(gè)父類的泛型對象,然后又在父類的泛型對象中,插入其他類型,那可就亂了(又回到
比如:
Listlist = 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ù),要么都在
中添加
這個(gè)主要是為了跟舊代碼兼容,對舊代碼進(jìn)行的一種類型安全檢查,防止將Cat插入Dog集合中這種錯(cuò)誤
這種檢查是發(fā)生在編譯階段,這樣就可以提早發(fā)現(xiàn)問題
對應(yīng)的類為Collections工具類,方法如下圖
代碼如下
// 動(dòng)態(tài)類型安全檢查,在與舊代碼兼容時(shí),防止將Dog放到Cat集合中類似的問題 // === 檢查之前 === List list = new ArrayList(); // 添加不報(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);
泛型的作用:
提高類型安全性:預(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)者集合場景
super T>
:超類型的通配符限定,以添加為主,比如生產(chǎn)者集合場景
感謝各位的閱讀,以上就是“Java中的泛型詳細(xì)介紹”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對Java中的泛型詳細(xì)介紹這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關(guān)知識點(diǎn)的文章,歡迎關(guān)注!