Java泛型的學(xué)習(xí)和使用

前面,由于對(duì)泛型擦除的思考,引出了對(duì)Java-Type體系的學(xué)習(xí)。本篇,就讓我們繼續(xù)對(duì)“泛型”進(jìn)行研究:

JDK1.5中引入了對(duì)Java語(yǔ)言的多種擴(kuò)展,泛型(generics)即其中之一。

1. 什么是泛型?

泛型,即“參數(shù)化類型”,就跟在方法或構(gòu)造函數(shù)中普通的參數(shù)一樣,當(dāng)一個(gè)方法被調(diào)用時(shí),實(shí)參替換形參,方法體被執(zhí)行。當(dāng)一個(gè)泛型聲明被調(diào)用,實(shí)際類型參數(shù)取代形式類型參數(shù)。

泛型

2. 為什么需要泛型?

對(duì)于Java開(kāi)發(fā)者來(lái)說(shuō),集合是泛型運(yùn)用最多的地方,例如:List<String>、Map<String,Integer>;試想一下,如若沒(méi)有泛型泛型,當(dāng)我們對(duì)集合進(jìn)行遍歷、進(jìn)行元素獲取的時(shí)候,一坨坨強(qiáng)制類型轉(zhuǎn)換的代碼就足以讓人發(fā)瘋,而且極易出現(xiàn)類型轉(zhuǎn)換失敗的風(fēng)險(xiǎn);

但是,泛型的出現(xiàn)解決了這個(gè)問(wèn)題,它不但簡(jiǎn)化了代碼,還提高了程序的安全性;類型轉(zhuǎn)換的錯(cuò)誤提前到編譯期解決掉;

強(qiáng)制轉(zhuǎn)換
類型轉(zhuǎn)換失敗

3. 泛型的擦除

JDK1.5版本推出了泛型機(jī)制,在此之前,Java語(yǔ)言中并沒(méi)有泛型的概念;當(dāng)新特性來(lái)到的時(shí)候,必然會(huì)引起新老代碼兼容性的問(wèn)題,泛型也不例外。Java為解決兼容性問(wèn)題,采用了擦除機(jī)制;

當(dāng)我們聲明并使用泛型的時(shí)候,編譯器會(huì)幫助我們進(jìn)行類型的檢查和推斷,然而在代碼完成編譯后的Class文件中,泛型信息卻不復(fù)存在了,JVM在運(yùn)行期間對(duì)泛型無(wú)感知,這樣新老代碼的兼容性迎刃而解,這也就是Java泛型的擦除;

在方法中,我們定義了List<String>、Map<String,Integer>等對(duì)象,在編譯結(jié)束之后,都會(huì)變成List、Map等原始類型;對(duì)于JVM來(lái)說(shuō),泛型的信息是不可見(jiàn)的;下面,我們通過(guò)反射,來(lái)觀察下!

反射

在程序運(yùn)行期間,泛型的約束并不存在,通過(guò)反射,可以向集合中添加任意類型對(duì)象;

此外,當(dāng)我們通過(guò)反編譯工具查看GenericTest.class文件的時(shí)候,發(fā)現(xiàn)ArrayList對(duì)象中的泛型沒(méi)有了,這也間接證明了泛型的擦除;

接下來(lái),我們?cè)谕ㄟ^(guò)javap命令查看生成的Class文件:


源碼
javap -c 命令

結(jié)果顯示,當(dāng)我們執(zhí)行集合的add方法的時(shí)候,泛型類型String已經(jīng)被擦除,取而代之的是Object類型;當(dāng)我們執(zhí)行g(shù)et方法的時(shí)候,泛型同樣不存在,也是被當(dāng)做Object來(lái)返回;

可是,我有個(gè)疑問(wèn),在編譯期由于泛型的存在,我們不需要顯式的進(jìn)行類型轉(zhuǎn)換,但是在運(yùn)行期間是如何解決的呢,難道不會(huì)報(bào)錯(cuò)嗎?

ArrayList--get方法
ArrayList--get方法

查看源碼發(fā)現(xiàn),ArrayList在get方法中,已經(jīng)顯式進(jìn)行了類型轉(zhuǎn)換;

自定義一個(gè)泛型類,在get方法中不進(jìn)行類型轉(zhuǎn)換的聲明,看看結(jié)果如何?

運(yùn)行main方法后,程序沒(méi)有報(bào)錯(cuò),正常結(jié)束;

通過(guò)上面的2個(gè)例子,我們不僅產(chǎn)生疑問(wèn),ArrayList中聲明了類型轉(zhuǎn)換,Test中沒(méi)有聲明,但是兩者在運(yùn)行期間都沒(méi)有報(bào)錯(cuò)?那么ArrayList的聲明意義何在呢 ?

當(dāng)再次查看ArrayList源碼時(shí)發(fā)現(xiàn),elementData對(duì)象實(shí)際上是一個(gè)Object類型數(shù)組,當(dāng)我們獲取元素并返回的時(shí)候,編譯器會(huì)根據(jù)方法的返回值進(jìn)行類型安全檢查,所以 return (E) elementData[index]才會(huì)有強(qiáng)制類型轉(zhuǎn)換的情況;

通過(guò)了解checkcast指令后,結(jié)合上面的2個(gè)例子,我認(rèn)為JVM虛擬機(jī)在真正執(zhí)行g(shù)et方法的時(shí)候,實(shí)際上隱式的為我們的代碼進(jìn)行了類型轉(zhuǎn)換操作,就好比在代碼中直接聲明String ss = (String)test.getT()、String sss = (String)list.get(0)一樣;

實(shí)際上,在了解到checkcast虛擬機(jī)指令后,再次證明了上面的觀點(diǎn);

checkcast:“檢驗(yàn)類型轉(zhuǎn)換,檢驗(yàn)未通過(guò)將拋出ClassCastException”;

官方解釋:checkcast checks that the top item on the operand stack (a reference to an object or array) can be cast to a given type. For example, if you write in Java:return ((String)obj);

4. 泛型擦除帶來(lái)的問(wèn)題

4.1 類型信息的丟失

由于泛型擦除機(jī)制的存在,在運(yùn)行期間無(wú)法獲取關(guān)于泛型參數(shù)類型的任何信息,自然也就無(wú)法對(duì)類型信息進(jìn)行操作;例如:instanceof 、創(chuàng)建對(duì)象等;

編譯報(bào)錯(cuò)

4.2 類型擦除與多態(tài)

首先,我們先復(fù)習(xí)下多態(tài)的概念,多態(tài)出現(xiàn)的場(chǎng)景;

簡(jiǎn)明直譯,多態(tài)多態(tài),多種形態(tài);接口下眾多的實(shí)現(xiàn)類,便是多態(tài)最顯著實(shí)現(xiàn)場(chǎng)景之一;

其次,還有方法的重寫Overriding和重載Overloading;

重寫Overriding是父類與子類之間多態(tài)性的一種表現(xiàn),如果在子類中定義某方法與其父類有相同的名稱和參數(shù),我們說(shuō)該方法被重寫(Overriding)。子類的對(duì)象使用這個(gè)方法時(shí),將調(diào)用子類中的定義,對(duì)它而言,父類中的定義如同被“屏蔽”了。

重載Overloading是一個(gè)類中多態(tài)性的一種表現(xiàn),如果在一個(gè)類中定義了多個(gè)同名的方法,它們或有不同的參數(shù)個(gè)數(shù)或有不同的參數(shù)類型,則稱為方法的重載(Overloading)。Overloaded的方法是可以改變返回值的類型但同時(shí)參數(shù)列表也得不同。

接下來(lái),讓我們看一個(gè)例子,來(lái)具體的分析;

父類Test


子類TestChild

由于泛型擦除的存在,在程序運(yùn)行期間,Test類在JVM虛擬機(jī)中實(shí)際的形態(tài)如下:

編譯后Test類

泛型被擦除,泛型變量替換為Object對(duì)象;接下來(lái),我們?cè)诳纯醋宇怲estChild代碼----setT:

@Override

public void setT(String s) {}

首先,來(lái)看看set方法,實(shí)際運(yùn)行期間父類Test的set方法參數(shù)為Object,子類的為String;回顧下Override
的定義,“如果在子類中定義某方法與其父類有相同的名稱和參數(shù),我們說(shuō)該方法被重寫(Overriding)”;顯然,在運(yùn)行期間我們子類和父類的set方法只有相同的名稱,并沒(méi)有相同的參數(shù),所以并不滿足“重寫”的定義;

在看下,重載的定義,“如果在一個(gè)類中定義了多個(gè)同名的方法,它們或有不同的參數(shù)個(gè)數(shù)或有不同的參數(shù)類型,則稱為方法的重載(Overloading)”。既然不是重寫,并且Test 和 TestChild又是子父類關(guān)系,那么set方法從定義上來(lái)看只有可能是重載的關(guān)系;子類繼承父類方法,在TestChild中形成重載:setT(Object t)、setT(String t);

既然我們推斷是setT屬于重載,那么就用代碼實(shí)現(xiàn)下即可:

測(cè)試重載

很不幸,編譯報(bào)錯(cuò),在子類中并沒(méi)有一個(gè)叫做setT(Object t)的方法,重載不成立,子類的方法依舊和父類屬于重寫關(guān)系;下面,讓我來(lái)進(jìn)一步去分析:

子類TestChild繼承了父類Test,并傳入泛型變量String,如果忽略泛型擦除的存在,父類Test代碼應(yīng)該變成這樣:

意淫下的父類

但實(shí)際上,Java在編譯期已經(jīng)將泛型變量擦除,運(yùn)行期間泛型變量變成了Object,沒(méi)有任何關(guān)于泛型String的信息;我們本意是實(shí)現(xiàn)方法的重寫,但實(shí)際上變成了重載(意淫下的重載);這下可如何是好?

于是,JVM虛擬機(jī)采用了一個(gè)特殊的方式來(lái)解決擦除和多態(tài)之間的矛盾,橋方法由此誕生;我們繼續(xù)使用javap -c 命令查看class文件;

子類TestChild

截圖中,子類TestChild實(shí)際上生成了4個(gè)方法,最下面的2個(gè)方法,就是JVM所生成的橋方法,而真正實(shí)現(xiàn)方法重寫的便是這個(gè)橋方法------------setT(Object t),而我們自己定義的@Oveerride注解只不過(guò)為了滿足編譯期的要求所存在的假象而已;

這樣一來(lái),虛擬機(jī)便解決了泛型擦?xí)投鄳B(tài)之間的矛盾;那么,get()是否存在上面重寫的問(wèn)題呢?

答案是NONONO!由于重寫(Overriding)只針對(duì)于方法名和方法參數(shù),并不沒(méi)有強(qiáng)調(diào)返回值的異同。所以子類---public String getT()父類---public Object getT() 是可以形成重寫的關(guān)系!

但是,在編譯之后的class文件中,由于橋方法的存在,子類中有了2個(gè)getT()方法,分別為public String getT()、public Object getT(),如果在我們實(shí)際定義方法的時(shí)候,在一個(gè)類中出現(xiàn)2個(gè)這樣的方法,是無(wú)法通過(guò)編譯器的檢查的!

同名方法

因?yàn)橐陨?個(gè)方法,違背了重載的定義,重名方法必須要有不同的形參,否則編譯器會(huì)報(bào)錯(cuò)!

但實(shí)際上由于橋方法是在編譯后的class文件中生成,所以我們認(rèn)為虛擬機(jī)是允許這樣的情況出現(xiàn),JVM虛擬機(jī)認(rèn)定方法唯一的方式,不單通過(guò)方法名稱和參數(shù),還包括了方法的返回值;

4.3 異常和泛型擦除

自定義異常類,還必須是帶有泛型的異常類;

編譯報(bào)錯(cuò)

自定義的泛型類并不能繼承exception,為什么?

歸根到底,還是由于泛型擦除的存在!如果上面編譯通過(guò),那么我們?cè)诖a中將會(huì)看到如下情形:

捕獲異常

由于泛型擦除的存在,GenericException在編譯之后將不存在泛型信息,2次catch的異常將會(huì)變成一樣,這在Java中是不允許存在的;

此外,還有一種情況,看如下代碼:

捕獲異常

由于泛型擦除的存在,T泛型變量在編譯之后將會(huì)變成Exception類型(由于extends的存在,此處不會(huì)變成Object);根據(jù)Java中關(guān)于捕捉異常的規(guī)則:子類異常必須在最前面,以此往后捕捉父類異常;所以說(shuō),以上代碼違背了Java異常規(guī)范,禁止在catch中使用泛型!


5. 自定義泛型接口、泛型類和泛型方法

5.1 泛型接口

泛型接口


泛型接口

5.2 泛型類

泛型類

值得注意的是,在泛型類中,成員變量不能使用靜態(tài)修飾,編譯報(bào)錯(cuò)!

靜態(tài)修飾成員變量

由于是靜態(tài)變量,不需要?jiǎng)?chuàng)建對(duì)象即可調(diào)用,無(wú)法確定泛型是哪種類型,所以編譯禁止通過(guò)!當(dāng)然,需要區(qū)分5.3章節(jié)中的情況:

5.3 泛型方法

泛型方法

在泛型方法中,自己定義的泛型變量,與類無(wú)關(guān);

6. 通配符與上下界

在我們實(shí)際工作中,常見(jiàn)的通配符有3類:

無(wú)限定通配符,形式:<?>

上邊界通配符,形式:<? extends Number>

下邊界通配符,形式:<? super Number>

泛型的通配符?與我們平常所定義的T 、K、V等泛型變量功能類似,但是通配符?只能使用在已聲明過(guò)泛型的類中,不能直接定義在類上,方法上,屬性上;

通配符的運(yùn)用

List<?> list代表著,可以向List中存入任何類型的對(duì)象,此時(shí)的?可以理解為Object;

那么,上邊界和下邊界又是什么意思呢?

<? extends Number>代表著所傳入的類型參數(shù)只能為Number的子類,這就是通配符的上邊界;

<? super Number>代表著所傳入的類型參數(shù)只能為Number、Number的父類,這就是通配符的下邊界;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • object 變量可指向任何類的實(shí)例,這讓你能夠創(chuàng)建可對(duì)任何數(shù)據(jù)類型進(jìn)程處理的類。然而,這種方法存在幾個(gè)嚴(yán)重的問(wèn)題...
    CarlDonitz閱讀 1,018評(píng)論 0 5
  • 泛型是Java 1.5引入的新特性。泛型的本質(zhì)是參數(shù)化類型,這種參數(shù)類型可以用在類、變量、接口和方法的創(chuàng)建中,分別...
    何時(shí)不晚閱讀 3,112評(píng)論 0 2
  • 在之前的文章中分析過(guò)了多態(tài),可以知道多態(tài)本身是一種泛化機(jī)制,它通過(guò)基類或者接口來(lái)設(shè)計(jì),使程序擁有一定的靈活性,但是...
    _小二_閱讀 752評(píng)論 0 0
  • 多年后,當(dāng)我坐在出租屋的桌前,夜半歌聲,悠揚(yáng)而輕快,執(zhí)筆逐字逐句回憶著過(guò)去,仿佛那些櫻花與柳絮就飛舞在眼前,音樂(lè)的...
    5a23edd886c0閱讀 572評(píng)論 1 2
  • 書(shū)名:《火花》 作者:又吉直樹(shù) 這是一部小說(shuō)又是一部“自傳”。故事站在主人公——漫才組合Sparks的德永——的角...
    鞠茜閱讀 400評(píng)論 0 0

友情鏈接更多精彩內(nèi)容