關于Java泛型這些事
關于泛型,有一道經(jīng)典的考題:
- public static void main(String[] args) {
- List<String> list1= new ArrayList<String>();
- List<Integer> list2= new ArrayList<Integer>();
- System.out.println(list1.getClass() == list2.getClass());
- }
請問上面代碼的輸出結果是什么?
如果是了解泛型的同學會很容易答出:true,如果是不了解泛型的同學則很可能會答錯。今天就和大家一起來重溫一下Java泛型相關的知識。
一、什么是泛型?
泛型(generics)是 JDK 5 中引入的一個新特性, 泛型提供了編譯時類型安全檢測機制,該機制允許程序員在編譯時檢測到非法的類型。泛型的本質(zhì)是參數(shù)化類型,也就是說所操作的數(shù)據(jù)類型被指定為一個參數(shù)。具有以下特點:
- 與普通的 Object 代替一切類型這樣簡單粗暴而言,泛型使得數(shù)據(jù)的類別可以像參數(shù)一樣由外部傳遞進來。它提供了一種擴展能力。它更符合面向抽象開發(fā)的軟件編程宗旨。
- 當具體的類型確定后,泛型又提供了一種類型檢測的機制,只有相匹配的數(shù)據(jù)才能正常的賦值,否則編譯器就不通過。所以說,它是一種類型安全檢測機制,一定程度上提高了軟件的安全性防止出現(xiàn)低級的失誤。
- 泛型提高了程序代碼的可讀性,不必要等到運行的時候才去強制轉(zhuǎn)換,在定義或者實例化階段,因為 Cache
這個類型顯化的效果,程序員能夠一目了然猜測出代碼要操作的數(shù)據(jù)類型。
泛型按照使用情況可以分為3種:泛型類、泛型方法、泛型接口。
1.泛型類
我們可以定義如下一個泛型類
- /**
- * @author machongjia
- * @date 2021/12/28 20:02
- * @description
- */
- public class Generic<T> {
- private T var;
- public Generic(T var) {
- this.var = var;
- }
- public T getVar() {
- return var;
- }
- public static void main(String[] args) {
- Generic<Integer> i = new Generic<Integer>(1000);
- Generic<String> s = new Generic<String>("hello");
- System.out.println(i.getVar());
- System.out.println(s.getVar());
- }
- }
輸出結果:
- 1000
- hello
常用的類似于T這樣的類型參數(shù)包括:
T:代表一般的任何類
E:代表 Element 的意思,或者 Exception 異常的意思
K:代表 Key 的意思。
V:代表 Value 的意思,通常與 K 一起配合使用
S:代表 Subtype 的意思
泛型類可以不止接受一個參數(shù)T,還可以接受多個參數(shù),類似于下面這種:
- public class Generic<E,T> {
- private E var1;
- private T var2;
- public Generic(E var1, T var2) {
- this.var1 = var1;
- this.var2 = var2;
- }
- public static void main(String[] args) {
- Generic<Integer,String> generic = new Generic<Integer,String>(1000,"hello");
- System.out.println(generic.var1);
- System.out.println(generic.var2);
- }
- }
2.泛型方法
- public class Generic {
- public <T> void testMethod(T t){
- }
- }
泛型方法與泛型類稍有不同的地方是,類型參數(shù)也就是尖括號那一部分是寫在返回值前面的。
當然,聲明的類型參數(shù),其實也是可以當作返回值的類型的。
泛型類與泛型方法共存的情況:
- public class Generic<T> {
- public void testMethod(T t){
- System.out.println(t.getClass().getName());
- }
- public <T> T testMethod1(T t){
- return t;
- }
- }
上面代碼中,Test1
3.泛型接口
泛型接口與泛型類的定義及使用基本相同。泛型接口常被用在各種類的生產(chǎn)器中,可以看一個例子:
- //定義一個泛型接口
- public interface Generator<T> {
- public T next();
- }
當實現(xiàn)泛型接口的類,未傳入泛型實參時:
- /**
- * 未傳入泛型實參時,與泛型類的定義相同,在聲明類的時候,需將泛型的聲明也一起加到類中
- * 即:class FruitGenerator<T> implements Generator<T>{
- * 如果不聲明泛型,如:class FruitGenerator implements Generator<T>,編譯器會報錯:"Unknown class"
- */
- class FruitGenerator<T> implements Generator<T>{
- @Override
- public T next() {
- return null;
- }
- }
當實現(xiàn)泛型接口的類,傳入泛型實參時:
- /**
- * 傳入泛型實參時,定義一個生產(chǎn)器實現(xiàn)這個接口,雖然我們只創(chuàng)建了一個泛型接口Generator<T>
- * 但是我們可以為T傳入無數(shù)個實參,形成無數(shù)種類型的Generator接口。
- * 在實現(xiàn)類實現(xiàn)泛型接口時,如已將泛型類型傳入實參類型,則所有使用泛型的地方都要替換成傳入的實參類型
- * 即:Generator<T>,public T next();中的的T都要替換成傳入的String類型。
- */
- public class FruitGenerator implements Generator<String> {
- private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
- @Override
- public String next() {
- Random rand = new Random();
- return fruits[rand.nextInt(3)];
- }
- }
4.通配符?
通配符的出現(xiàn)是為了指定泛型中的類型范圍,包含以下3 種形式。
- <?>被稱作無限定的通配符。
- <? extends T>被稱作有上限的通配符。
- <? super T>被稱作有下限的通配符。
無限定通配符<?>
無限定通配符經(jīng)常與容器類配合使用,它其中的 ? 其實代表的是未知類型,所以涉及到 ? 時的操作,一定與具體類型無關。
- public void testWildCards(Collection<?> collection){
- }
上面的代碼中,方法內(nèi)的參數(shù)是被無限定通配符修飾的 Collection 對象,它隱略地表達了一個意圖或者可以說是限定,那就是 testWidlCards() 這個方法內(nèi)部無需關注 Collection 中的真實類型,因為它是未知的。所以,你只能調(diào)用 Collection 中與類型無關的方法。
和相對應,前者?代表類型T及T的子類,后者?代表T及T的超類。
值得注意的是,如果用泛型方法來取代通配符,那么上面代碼中 collection 是能夠進行寫操作的。只不過要進行強制轉(zhuǎn)換。
二、什么是泛型的類型擦除?
Java泛型這個特性是從JDK 1.5才開始加入的,因此為了兼容之前的版本,Java泛型的實現(xiàn)采取了“偽泛型”的策略,即Java在語法上支持泛型,但是在編譯階段會進行所謂的“類型擦除”(Type Erasure),將所有的泛型表示(尖括號中的內(nèi)容)都替換為具體的類型(其對應的原生態(tài)類型),就像完全沒有泛型一樣。理解類型擦除對于用好泛型是很有幫助的,尤其是一些看起來“疑難雜癥”的問題,弄明白了類型擦除也就迎刃而解了。
- 泛型的類型擦除原則是:
- 消除類型參數(shù)聲明,即刪除<>及其包圍的部分。
- 根據(jù)類型參數(shù)的上下界推斷并替換所有的類型參數(shù)為原生態(tài)類型:如果類型參數(shù)是無限制通配符或沒有上下界限定則替換為Object,如果存在上下界限定則根據(jù)子類替換原則取類型參數(shù)的最左邊限定類型(即父類)。
- 為了保證類型安全,必要時插入強制類型轉(zhuǎn)換代碼。
- 自動產(chǎn)生“橋接方法”以保證擦除類型后的代碼仍然具有泛型的“多態(tài)性”。
1.類型擦除做了什么?
上面我們說了,編譯完成后會對泛型進行類型擦除,如果想要眼見為實,實際看一下的話應該怎么辦呢?那么就需要對編譯后的字節(jié)碼文件進行反編譯了,這里使用一個輕量級的小工具Jad來進行反編譯,Jad的使用也很簡單,下載解壓后,把需要反編譯的字節(jié)碼文件放在目錄下,然后在命令行里執(zhí)行下面的命令就可以在同目錄下生成反編譯后的.java文件了:
- jad -sjava Test.class
好了,工具準備好了,下面我們就看一下不同情況下的類型擦除。
無限制類型擦除
當類定義中的類型參數(shù)沒有任何限制時,在類型擦除后,會被直接替換為Object。在下面的例子中,
有限制類型擦除
當類定義中的類型參數(shù)存在限制時,在類型擦除中替換為類型參數(shù)的上界或者下界。下面的代碼中,經(jīng)過擦除后T被替換成了Integer:
擦除方法中的類型參數(shù)
比較下面兩邊的代碼,可以看到在擦除方法中的類型參數(shù)時,和擦除類定義中的類型參數(shù)一致,無限制時直接擦除為Object,有限制時則會被擦除為上界或下界:
2.類型擦除帶來了哪些局限性?
類型擦除,是泛型能夠與之前的 java 版本代碼兼容共存的原因。但也因為類型擦除,它會抹掉很多繼承相關的特性,這是它帶來的局限性。
理解類型擦除有利于我們繞過開發(fā)當中可能遇到的雷區(qū),同樣理解類型擦除也能讓我們繞過泛型本身的一些限制。比如
正常情況下,因為泛型的限制,編譯器不讓最后一行代碼編譯通過,因為類似不匹配,但是,基于對類型擦除的了解,利用反射,我們可以繞過這個限制。
- public interface List<E> extends Collection<E>{
- boolean add(E e);
- }
上面是 List 和其中的 add() 方法的源碼定義。
因為 E 代表任意的類型,所以類型擦除時,add 方法其實等同于:
- boolean add(Object obj);
那么,利用反射,我們繞過編譯器去調(diào)用 add 方法。
- public class ToolTest {
- public static void main(String[] args) {
- List<Integer> ls = new ArrayList<>();
- ls.add(23);
- // ls.add("text");
- try {
- Method method = ls.getClass().getDeclaredMethod("add",Object.class);
- method.invoke(ls,"test");
- method.invoke(ls,42.9f);
- } catch (NoSuchMethodException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (SecurityException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IllegalAccessException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IllegalArgumentException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (InvocationTargetException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- for ( Object o: ls){
- System.out.println(o);
- }
- }
- }
打印結果是:
- 23
- test
- 42.9
- 1
- 2
- 3
可以看到,利用類型擦除的原理,用反射的手段就繞過了正常開發(fā)中編譯器不允許的操作限制。