前面木羹,由于對泛型擦除的思考,引出了對Java-Type體系的學(xué)習(xí)解孙。本篇坑填,就讓我們繼續(xù)對“泛型”進行研究:
JDK1.5中引入了對Java語言的多種擴展,泛型(generics)即其中之一弛姜。
1. 什么是泛型脐瑰?
泛型,即“參數(shù)化類型”廷臼,就跟在方法或構(gòu)造函數(shù)中普通的參數(shù)一樣蚪黑,當(dāng)一個方法被調(diào)用時,實參替換形參中剩,方法體被執(zhí)行忌穿。當(dāng)一個泛型聲明被調(diào)用,實際類型參數(shù)取代形式類型參數(shù)结啼。
2. 為什么需要泛型掠剑?
對于Java開發(fā)者來說,集合是泛型運用最多的地方郊愧,例如:List<String>朴译、Map<String,Integer>;試想一下属铁,如若沒有泛型泛型眠寿,當(dāng)我們對集合進行遍歷、進行元素獲取的時候焦蘑,一坨坨強制類型轉(zhuǎn)換的代碼就足以讓人發(fā)瘋盯拱,而且極易出現(xiàn)類型轉(zhuǎn)換失敗的風(fēng)險;
但是,泛型的出現(xiàn)解決了這個問題狡逢,它不但簡化了代碼宁舰,還提高了程序的安全性;類型轉(zhuǎn)換的錯誤提前到編譯期解決掉奢浑;
3. 泛型的擦除
JDK1.5版本推出了泛型機制蛮艰,在此之前,Java語言中并沒有泛型的概念雀彼;當(dāng)新特性來到的時候壤蚜,必然會引起新老代碼兼容性的問題,泛型也不例外徊哑。Java為解決兼容性問題仍律,采用了擦除機制;
當(dāng)我們聲明并使用泛型的時候实柠,編譯器會幫助我們進行類型的檢查和推斷水泉,然而在代碼完成編譯后的Class文件中,泛型信息卻不復(fù)存在了窒盐,JVM在運行期間對泛型無感知草则,這樣新老代碼的兼容性迎刃而解,這也就是Java泛型的擦除蟹漓;
在方法中炕横,我們定義了List<String>、Map<String,Integer>等對象葡粒,在編譯結(jié)束之后份殿,都會變成List、Map等原始類型嗽交;對于JVM來說卿嘲,泛型的信息是不可見的;下面夫壁,我們通過反射拾枣,來觀察下!
在程序運行期間盒让,泛型的約束并不存在梅肤,通過反射,可以向集合中添加任意類型對象邑茄;
此外姨蝴,當(dāng)我們通過反編譯工具查看GenericTest.class文件的時候,發(fā)現(xiàn)ArrayList對象中的泛型沒有了肺缕,這也間接證明了泛型的擦除左医;
接下來授帕,我們在通過javap命令查看生成的Class文件:
結(jié)果顯示,當(dāng)我們執(zhí)行集合的add方法的時候炒辉,泛型類型String已經(jīng)被擦除豪墅,取而代之的是Object類型泉手;當(dāng)我們執(zhí)行g(shù)et方法的時候黔寇,泛型同樣不存在,也是被當(dāng)做Object來返回斩萌;
可是缝裤,我有個疑問,在編譯期由于泛型的存在颊郎,我們不需要顯式的進行類型轉(zhuǎn)換憋飞,但是在運行期間是如何解決的呢,難道不會報錯嗎姆吭?
查看源碼發(fā)現(xiàn)榛做,ArrayList在get方法中,已經(jīng)顯式進行了類型轉(zhuǎn)換内狸;
自定義一個泛型類检眯,在get方法中不進行類型轉(zhuǎn)換的聲明,看看結(jié)果如何昆淡?
運行main方法后锰瘸,程序沒有報錯,正常結(jié)束昂灵;
通過上面的2個例子避凝,我們不僅產(chǎn)生疑問,ArrayList中聲明了類型轉(zhuǎn)換眨补,Test中沒有聲明管削,但是兩者在運行期間都沒有報錯?那么ArrayList的聲明意義何在呢 ?
當(dāng)再次查看ArrayList源碼時發(fā)現(xiàn)撑螺,elementData對象實際上是一個Object類型數(shù)組佩谣,當(dāng)我們獲取元素并返回的時候,編譯器會根據(jù)方法的返回值進行類型安全檢查实蓬,所以 return (E) elementData[index]才會有強制類型轉(zhuǎn)換的情況茸俭;
通過了解checkcast指令后,結(jié)合上面的2個例子安皱,我認(rèn)為JVM虛擬機在真正執(zhí)行g(shù)et方法的時候调鬓,實際上隱式的為我們的代碼進行了類型轉(zhuǎn)換操作,就好比在代碼中直接聲明String ss = (String)test.getT()酌伊、String sss = (String)list.get(0)一樣腾窝;
實際上缀踪,在了解到checkcast虛擬機指令后,再次證明了上面的觀點虹脯;
checkcast:“檢驗類型轉(zhuǎn)換驴娃,檢驗未通過將拋出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. 泛型擦除帶來的問題
4.1 類型信息的丟失
由于泛型擦除機制的存在循集,在運行期間無法獲取關(guān)于泛型參數(shù)類型的任何信息唇敞,自然也就無法對類型信息進行操作;例如:instanceof 咒彤、創(chuàng)建對象等疆柔;
4.2 類型擦除與多態(tài)
首先,我們先復(fù)習(xí)下多態(tài)的概念镶柱,多態(tài)出現(xiàn)的場景旷档;
簡明直譯,多態(tài)多態(tài)歇拆,多種形態(tài)鞋屈;接口下眾多的實現(xiàn)類,便是多態(tài)最顯著實現(xiàn)場景之一故觅;
其次厂庇,還有方法的重寫Overriding和重載Overloading;
重寫Overriding是父類與子類之間多態(tài)性的一種表現(xiàn)逻卖,如果在子類中定義某方法與其父類有相同的名稱和參數(shù)宋列,我們說該方法被重寫(Overriding)。子類的對象使用這個方法時评也,將調(diào)用子類中的定義炼杖,對它而言,父類中的定義如同被“屏蔽”了盗迟。
重載Overloading是一個類中多態(tài)性的一種表現(xiàn)坤邪,如果在一個類中定義了多個同名的方法,它們或有不同的參數(shù)個數(shù)或有不同的參數(shù)類型罚缕,則稱為方法的重載(Overloading)艇纺。Overloaded的方法是可以改變返回值的類型但同時參數(shù)列表也得不同。
接下來邮弹,讓我們看一個例子黔衡,來具體的分析;
由于泛型擦除的存在腌乡,在程序運行期間盟劫,Test類在JVM虛擬機中實際的形態(tài)如下:
泛型被擦除,泛型變量替換為Object對象与纽;接下來侣签,我們在看看子類TestChild代碼----setT:
@Override
public void setT(String s) {}
首先塘装,來看看set方法,實際運行期間父類Test的set方法參數(shù)為Object影所,子類的為String蹦肴;回顧下Override
的定義,“如果在子類中定義某方法與其父類有相同的名稱和參數(shù)猴娩,我們說該方法被重寫(Overriding)”阴幌;顯然,在運行期間我們子類和父類的set方法只有相同的名稱胀溺,并沒有相同的參數(shù)裂七,所以并不滿足“重寫”的定義皆看;
在看下仓坞,重載的定義,“如果在一個類中定義了多個同名的方法腰吟,它們或有不同的參數(shù)個數(shù)或有不同的參數(shù)類型无埃,則稱為方法的重載(Overloading)”。既然不是重寫毛雇,并且Test 和 TestChild又是子父類關(guān)系嫉称,那么set方法從定義上來看只有可能是重載的關(guān)系;子類繼承父類方法灵疮,在TestChild中形成重載:setT(Object t)织阅、setT(String t);
既然我們推斷是setT屬于重載震捣,那么就用代碼實現(xiàn)下即可:
很不幸荔棉,編譯報錯,在子類中并沒有一個叫做setT(Object t)的方法蒿赢,重載不成立润樱,子類的方法依舊和父類屬于重寫關(guān)系;下面羡棵,讓我來進一步去分析:
子類TestChild繼承了父類Test壹若,并傳入泛型變量String,如果忽略泛型擦除的存在皂冰,父類Test代碼應(yīng)該變成這樣:
但實際上店展,Java在編譯期已經(jīng)將泛型變量擦除,運行期間泛型變量變成了Object秃流,沒有任何關(guān)于泛型String的信息赂蕴;我們本意是實現(xiàn)方法的重寫,但實際上變成了重載(意淫下的重載)剔应;這下可如何是好睡腿?
于是语御,JVM虛擬機采用了一個特殊的方式來解決擦除和多態(tài)之間的矛盾,橋方法由此誕生席怪;我們繼續(xù)使用javap -c 命令查看class文件应闯;
截圖中,子類TestChild實際上生成了4個方法挂捻,最下面的2個方法碉纺,就是JVM所生成的橋方法,而真正實現(xiàn)方法重寫的便是這個橋方法------------setT(Object t)刻撒,而我們自己定義的@Oveerride注解只不過為了滿足編譯期的要求所存在的假象而已骨田;
這樣一來,虛擬機便解決了泛型擦?xí)投鄳B(tài)之間的矛盾声怔;那么态贤,get()是否存在上面重寫的問題呢?
答案是NONONO醋火!由于重寫(Overriding)只針對于方法名和方法參數(shù)悠汽,并不沒有強調(diào)返回值的異同。所以子類---public String getT() 和 父類---public Object getT() 是可以形成重寫的關(guān)系芥驳!
但是柿冲,在編譯之后的class文件中,由于橋方法的存在兆旬,子類中有了2個getT()方法假抄,分別為public String getT()、public Object getT()丽猬,如果在我們實際定義方法的時候宿饱,在一個類中出現(xiàn)2個這樣的方法,是無法通過編譯器的檢查的宝鼓!
因為以上2個方法刑棵,違背了重載的定義,重名方法必須要有不同的形參愚铡,否則編譯器會報錯蛉签!
但實際上由于橋方法是在編譯后的class文件中生成,所以我們認(rèn)為虛擬機是允許這樣的情況出現(xiàn)沥寥,JVM虛擬機認(rèn)定方法唯一的方式碍舍,不單通過方法名稱和參數(shù),還包括了方法的返回值邑雅;
4.3 異常和泛型擦除
自定義異常類片橡,還必須是帶有泛型的異常類;
自定義的泛型類并不能繼承exception淮野,為什么捧书?
歸根到底吹泡,還是由于泛型擦除的存在!如果上面編譯通過经瓷,那么我們在代碼中將會看到如下情形:
由于泛型擦除的存在爆哑,GenericException在編譯之后將不存在泛型信息,2次catch的異常將會變成一樣舆吮,這在Java中是不允許存在的揭朝;
此外,還有一種情況色冀,看如下代碼:
由于泛型擦除的存在潭袱,T泛型變量在編譯之后將會變成Exception類型(由于extends的存在,此處不會變成Object)锋恬;根據(jù)Java中關(guān)于捕捉異常的規(guī)則:子類異常必須在最前面屯换,以此往后捕捉父類異常;所以說伶氢,以上代碼違背了Java異常規(guī)范趟径,禁止在catch中使用泛型瘪吏!
5. 自定義泛型接口癣防、泛型類和泛型方法
5.1 泛型接口
5.2 泛型類
值得注意的是,在泛型類中掌眠,成員變量不能使用靜態(tài)修飾蕾盯,編譯報錯!
由于是靜態(tài)變量蓝丙,不需要創(chuàng)建對象即可調(diào)用级遭,無法確定泛型是哪種類型,所以編譯禁止通過渺尘!當(dāng)然挫鸽,需要區(qū)分5.3章節(jié)中的情況:
5.3 泛型方法
在泛型方法中,自己定義的泛型變量鸥跟,與類無關(guān)丢郊;
6. 通配符與上下界
在我們實際工作中,常見的通配符有3類:
無限定通配符医咨,形式:<?>
上邊界通配符枫匾,形式:<? extends Number>
下邊界通配符,形式:<? super Number>
泛型的通配符拟淮?與我們平常所定義的T 干茉、K、V等泛型變量功能類似很泊,但是通配符角虫?只能使用在已聲明過泛型的類中沾谓,不能直接定義在類上,方法上戳鹅,屬性上搏屑;
List<?> list代表著,可以向List中存入任何類型的對象粉楚,此時的辣恋?可以理解為Object;
那么模软,上邊界和下邊界又是什么意思呢伟骨?
<? extends Number>代表著所傳入的類型參數(shù)只能為Number的子類,這就是通配符的上邊界燃异;
<? super Number>代表著所傳入的類型參數(shù)只能為Number携狭、Number的父類,這就是通配符的下邊界回俐;