前面,由于對(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ò)誤提前到編譯期解決掉;


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文件:


結(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ò)嗎?


查看源碼發(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ì)象等;

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)具體的分析;


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

泛型被擦除,泛型變量替換為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)下即可:

很不幸,編譯報(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實(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 異常和泛型擦除
自定義異常類,還必須是帶有泛型的異常類;

自定義的泛型類并不能繼承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)變量,不需要?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ò)泛型的類中,不能直接定義在類上,方法上,屬性上;

List<?> list代表著,可以向List中存入任何類型的對(duì)象,此時(shí)的?可以理解為Object;
那么,上邊界和下邊界又是什么意思呢?
<? extends Number>代表著所傳入的類型參數(shù)只能為Number的子類,這就是通配符的上邊界;
<? super Number>代表著所傳入的類型參數(shù)只能為Number、Number的父類,這就是通配符的下邊界;