目錄
一、概述
1、定義:Java泛型(generics
)泛型是Java SE 1.5的新特性,泛型的本質是參數化類型,也就是所操作的數據類型被指定爲一個參數。這種參數類型可以用在類、接口和方法的創建中,分別稱爲泛型類、泛型接口、泛型方法。
2、優勢:Java語言引入泛型的優勢在於安全、重用。 泛型在編譯的時候檢查類型安全,並且所有的強制轉換都是自動和隱式的,以提高代碼的重用率。
3、作用:相比雜亂地使用Object聲明
變量,再進行強制類型轉換的代碼而言,使用泛型機制編寫的程序代碼具有更好的安全性和可讀性。泛型對於集合類尤其有用,例如,List<T>容器
就是一個無處不在的泛型集合類。
List<String> list=new ArrayList<>();
二、<T>和<?>的使用及區別
好奇List<T>和List<?>有什麼區別呢?其實這個問題就是問無限定類型變量“<T>”和無界通配符“<?>”的區別。
討論“<T>"和"<?>",首先要區分開兩種不同的場景:
- 第一,聲明泛型類或泛型方法。
- 第二,使用泛型類或泛型方法。
類型參數“<T>”主要用於第一種,聲明泛型類或泛型方法; 無界通配符“<?>”主要用於第二種,使用泛型類或泛型方法。
(1)類型參數<T>
1、<T>聲明泛型類
List<T>最應該出現的地方,應該是定義一個泛型List容器。看看ArrayList的源碼:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess,
Cloneable, java.o.Serializable {
... ...
}
ArrayList<E>中的“E”也是類型參數。只是表示容器中元素Element的時候,習慣用“E”。
換一個簡單的例子,我們自己定義一個新泛型容器叫Box<T>。
class Box<T>{
private T item1;
private T item2;
}
<T>類型參數能夠對類型起到‘約束’的作用,例子中就是爲了保證Box裏的item1, item2都是同一個類型T。如,Box<String>,代表兩個item都是String;Box<Integer>裏兩個item都是Integer。
*那什麼時候會使用泛型類呢?
答:就是作爲泛型類的成員字段或成員方法的參數間接出現。
class Box<T>{
private List<T> item;//泛型類成員字段
public List<T> get(){return item;}//方法返回值
public void set(List<T> t){item=t;}//方法參數
}
這裏寫成List<T>爲了表示和Box<T>類型參數保持一致。
2、<T>聲明泛型方法
Function類的reduce是個靜態泛型方法,注意這方法是在普通類中定義的,而不是在泛型類中定義的。這裏的List<T>出現在reduce方法參數\函數返回值\函數內部,也是爲了保持泛型類型的一致性。
class Fuction{
public static <T> List<T> reduce(List<T> list){
}
}
注意,類型變量<T> 放在修飾符(public static
)的後面,返回類型的前面。
(2)無界通配符<?>
1、聲明泛型類不能用<?>
首先,要明確通配符<?>不能拿來聲明泛型。錯誤案例,如下:
class Box<?>{
//Error Example
private ? item1;
private ? item2;
}
通配符<?>作用:是使用定義好的泛型類。
例如,用<?>聲明List容器的變量類型,然後用一個實例對象給它賦值的時候就比較靈活。
List<?> list = new ArrayList<String>();
2、 <?>的CAP#1問題
List<?>這個寫法非常坑。因爲,通配符<?>會捕獲具體的String類型,但編譯器不叫它String,而是起個臨時的代號,比如”CAP#1“。因此,list不能存String,任何元素都不行,只能存null。
List<?> list = new ArrayList<String>();
list.add("hello"); //ERROR
//argument mismatch; String cannot be converted to CAP#1
list.add(111); //ERROR
//argument mismatch; int cannot be converted to CAP#1
另外,如果拿List<?>做參數,也會有奇妙的事情發生。還是剛纔Box<T>的例子,有get()和set()兩個方
class Box<T>{
private List<T> item;
public List<T> get(){return item;}
public void set(List<T> t){item=t;}
//把item取出來,再放回去, ERROR
public void getSet(Box<?> box){box.set(box.get());
//error: incompatible types: Object cannot be converted to CAP#1
}
}
新的getSet()方法,只是把item先用get()方法讀出來,然後再用set()方法存回去。按理說不可能有問題。實際運行卻會報錯。原因和前面一樣,用通配符<?>聲明的類型泛型類Box,調用box.set()的參數類型被編譯器捕獲,命名爲'CAP#1',和box<?>.get()返回的Object對象無法匹配。
3、解決方法
通過寫一個helper()輔助函數。
class Box<T>{
private List<T> item;
public List<T> get(){return item;}
public void set(List<T> t){item=t;}
//getSet()方法存取元素
public void getSet(Box<?> box){helper(box);}
//helper()輔助函數
public <V> void helper(Box<V> box){box.set(box.get());}
}
三、有界通配符<? extends X>、<? super X>
常用的是<? extends X>或者<? super XXX>兩種,帶有上下界的通配符。
其中,上界可以有多層<? extends X extends XX>。
注意:
- 對於無界通配符<?> extends 的通配符限定泛型,我們無法向裏面添加元素(只可以添加null),只能讀取其中的元素。
- 對於無界通配符<?> super 的通配符限定泛型,我們可以讀取其中的元素,但讀取出來的元素會變爲 Object 類型。
四、泛型擦除
Java的泛型基本上都是在編譯器這個層次上實現的,在運行期生成的字節碼中是不包含泛型中的類型信息的,使用泛型的時候加上類型參數,在編譯器編譯的時候會去掉,這個過程成爲類型擦除。
我們先看一個例子:
ArrayList<String> list1 = new ArrayList<String>();
list1.add("abc");
ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(123);
System.out.println(list1.getClass() == list2.getClass());
打印結果:true
結果顯示list1和list2居然是同一個類型的ArrayList,在運行時我們傳入的類型變量String和Integer都被擦除掉了,只剩下原始類型。原因是Java語言泛型在設計的時候爲了兼容原來的舊代碼,虛擬機並不知道泛型的存在,因爲泛型在編譯階段就已經被處理成普通的類和方法了。類型擦除規則:
-
若泛型類型沒有指定具體類型,用Object作爲原始類型;
-
若有限定類型< T exnteds XClass >,使用XClass作爲原始類型;
-
若有多個限定< T exnteds XClass1 & XClass2 >,使用第一個邊界類型XClass1作爲原始類型;