前面,由于對泛型擦除的思考,引出了對Java-Type體系的學習。本篇,就讓我們繼續對“泛型”進行研究:
JDK1.5中引入了對Java語言的多種擴展,泛型(generics)即其中之一。
1. 什么是泛型?
泛型,即“參數化類型”,就跟在方法或構造函數中普通的參數一樣,當一個方法被調用時,實參替換形參,方法體被執行。當一個泛型聲明被調用,實際類型參數取代形式類型參數。
2. 為什么需要泛型?
對于Java開發者來說,集合是泛型運用最多的地方,例如:List<String>、Map<String,Integer>;試想一下,如若沒有泛型泛型,當我們對集合進行遍歷、進行元素獲取的時候,一坨坨強制類型轉換的代碼就足以讓人發瘋,而且極易出現類型轉換失敗的風險;
但是,泛型的出現解決了這個問題,它不但簡化了代碼,還提高了程序的安全性;類型轉換的錯誤提前到編譯期解決掉;
3. 泛型的擦除
JDK1.5版本推出了泛型機制,在此之前,Java語言中并沒有泛型的概念;當新特性來到的時候,必然會引起新老代碼兼容性的問題,泛型也不例外。Java為解決兼容性問題,采用了擦除機制;
當我們聲明并使用泛型的時候,編譯器會幫助我們進行類型的檢查和推斷,然而在代碼完成編譯后的Class文件中,泛型信息卻不復存在了,JVM在運行期間對泛型無感知,這樣新老代碼的兼容性迎刃而解,這也就是Java泛型的擦除;
在方法中,我們定義了List<String>、Map<String,Integer>等對象,在編譯結束之后,都會變成List、Map等原始類型;對于JVM來說,泛型的信息是不可見的;下面,我們通過反射,來觀察下!
在程序運行期間,泛型的約束并不存在,通過反射,可以向集合中添加任意類型對象;
此外,當我們通過反編譯工具查看GenericTest.class文件的時候,發現ArrayList對象中的泛型沒有了,這也間接證明了泛型的擦除;
接下來,我們在通過javap命令查看生成的Class文件:
結果顯示,當我們執行集合的add方法的時候,泛型類型String已經被擦除,取而代之的是Object類型;當我們執行get方法的時候,泛型同樣不存在,也是被當做Object來返回;
可是,我有個疑問,在編譯期由于泛型的存在,我們不需要顯式的進行類型轉換,但是在運行期間是如何解決的呢,難道不會報錯嗎?
查看源碼發現,ArrayList在get方法中,已經顯式進行了類型轉換;
自定義一個泛型類,在get方法中不進行類型轉換的聲明,看看結果如何?
運行main方法后,程序沒有報錯,正常結束;
通過上面的2個例子,我們不僅產生疑問,ArrayList中聲明了類型轉換,Test中沒有聲明,但是兩者在運行期間都沒有報錯?那么ArrayList的聲明意義何在呢 ?
當再次查看ArrayList源碼時發現,elementData對象實際上是一個Object類型數組,當我們獲取元素并返回的時候,編譯器會根據方法的返回值進行類型安全檢查,所以 return (E) elementData[index]才會有強制類型轉換的情況;
通過了解checkcast指令后,結合上面的2個例子,我認為JVM虛擬機在真正執行get方法的時候,實際上隱式的為我們的代碼進行了類型轉換操作,就好比在代碼中直接聲明String ss = (String)test.getT()、String sss = (String)list.get(0)一樣;
實際上,在了解到checkcast虛擬機指令后,再次證明了上面的觀點;
checkcast:“檢驗類型轉換,檢驗未通過將拋出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 類型信息的丟失
由于泛型擦除機制的存在,在運行期間無法獲取關于泛型參數類型的任何信息,自然也就無法對類型信息進行操作;例如:instanceof 、創建對象等;
4.2 類型擦除與多態
首先,我們先復習下多態的概念,多態出現的場景;
簡明直譯,多態多態,多種形態;接口下眾多的實現類,便是多態最顯著實現場景之一;
其次,還有方法的重寫Overriding和重載Overloading;
重寫Overriding是父類與子類之間多態性的一種表現,如果在子類中定義某方法與其父類有相同的名稱和參數,我們說該方法被重寫(Overriding)。子類的對象使用這個方法時,將調用子類中的定義,對它而言,父類中的定義如同被“屏蔽”了。
重載Overloading是一個類中多態性的一種表現,如果在一個類中定義了多個同名的方法,它們或有不同的參數個數或有不同的參數類型,則稱為方法的重載(Overloading)。Overloaded的方法是可以改變返回值的類型但同時參數列表也得不同。
接下來,讓我們看一個例子,來具體的分析;
由于泛型擦除的存在,在程序運行期間,Test類在JVM虛擬機中實際的形態如下:
泛型被擦除,泛型變量替換為Object對象;接下來,我們在看看子類TestChild代碼----setT:
@Override
public void setT(String s) {}
首先,來看看set方法,實際運行期間父類Test的set方法參數為Object,子類的為String;回顧下Override
的定義,“如果在子類中定義某方法與其父類有相同的名稱和參數,我們說該方法被重寫(Overriding)”;顯然,在運行期間我們子類和父類的set方法只有相同的名稱,并沒有相同的參數,所以并不滿足“重寫”的定義;
在看下,重載的定義,“如果在一個類中定義了多個同名的方法,它們或有不同的參數個數或有不同的參數類型,則稱為方法的重載(Overloading)”。既然不是重寫,并且Test 和 TestChild又是子父類關系,那么set方法從定義上來看只有可能是重載的關系;子類繼承父類方法,在TestChild中形成重載:setT(Object t)、setT(String t);
既然我們推斷是setT屬于重載,那么就用代碼實現下即可:
很不幸,編譯報錯,在子類中并沒有一個叫做setT(Object t)的方法,重載不成立,子類的方法依舊和父類屬于重寫關系;下面,讓我來進一步去分析:
子類TestChild繼承了父類Test,并傳入泛型變量String,如果忽略泛型擦除的存在,父類Test代碼應該變成這樣:
但實際上,Java在編譯期已經將泛型變量擦除,運行期間泛型變量變成了Object,沒有任何關于泛型String的信息;我們本意是實現方法的重寫,但實際上變成了重載(意淫下的重載);這下可如何是好?
于是,JVM虛擬機采用了一個特殊的方式來解決擦除和多態之間的矛盾,橋方法由此誕生;我們繼續使用javap -c 命令查看class文件;
截圖中,子類TestChild實際上生成了4個方法,最下面的2個方法,就是JVM所生成的橋方法,而真正實現方法重寫的便是這個橋方法------------setT(Object t),而我們自己定義的@Oveerride注解只不過為了滿足編譯期的要求所存在的假象而已;
這樣一來,虛擬機便解決了泛型擦書和多態之間的矛盾;那么,get()是否存在上面重寫的問題呢?
答案是NONONO!由于重寫(Overriding)只針對于方法名和方法參數,并不沒有強調返回值的異同。所以子類---public String getT() 和 父類---public Object getT() 是可以形成重寫的關系!
但是,在編譯之后的class文件中,由于橋方法的存在,子類中有了2個getT()方法,分別為public String getT()、public Object getT(),如果在我們實際定義方法的時候,在一個類中出現2個這樣的方法,是無法通過編譯器的檢查的!
因為以上2個方法,違背了重載的定義,重名方法必須要有不同的形參,否則編譯器會報錯!
但實際上由于橋方法是在編譯后的class文件中生成,所以我們認為虛擬機是允許這樣的情況出現,JVM虛擬機認定方法唯一的方式,不單通過方法名稱和參數,還包括了方法的返回值;
4.3 異常和泛型擦除
自定義異常類,還必須是帶有泛型的異常類;
自定義的泛型類并不能繼承exception,為什么?
歸根到底,還是由于泛型擦除的存在!如果上面編譯通過,那么我們在代碼中將會看到如下情形:
由于泛型擦除的存在,GenericException在編譯之后將不存在泛型信息,2次catch的異常將會變成一樣,這在Java中是不允許存在的;
此外,還有一種情況,看如下代碼:
由于泛型擦除的存在,T泛型變量在編譯之后將會變成Exception類型(由于extends的存在,此處不會變成Object);根據Java中關于捕捉異常的規則:子類異常必須在最前面,以此往后捕捉父類異常;所以說,以上代碼違背了Java異常規范,禁止在catch中使用泛型!
5. 自定義泛型接口、泛型類和泛型方法
5.1 泛型接口
5.2 泛型類
值得注意的是,在泛型類中,成員變量不能使用靜態修飾,編譯報錯!
由于是靜態變量,不需要創建對象即可調用,無法確定泛型是哪種類型,所以編譯禁止通過!當然,需要區分5.3章節中的情況:
5.3 泛型方法
在泛型方法中,自己定義的泛型變量,與類無關;
6. 通配符與上下界
在我們實際工作中,常見的通配符有3類:
無限定通配符,形式:<?>
上邊界通配符,形式:<? extends Number>
下邊界通配符,形式:<? super Number>
泛型的通配符?與我們平常所定義的T 、K、V等泛型變量功能類似,但是通配符?只能使用在已聲明過泛型的類中,不能直接定義在類上,方法上,屬性上;
List<?> list代表著,可以向List中存入任何類型的對象,此時的?可以理解為Object;
那么,上邊界和下邊界又是什么意思呢?
<? extends Number>代表著所傳入的類型參數只能為Number的子類,這就是通配符的上邊界;
<? super Number>代表著所傳入的類型參數只能為Number、Number的父類,這就是通配符的下邊界;