Java 泛型使用與泛型擦除
Java 泛型
泛型(generics)是Java 1.5 中引入的特性。泛型的引入使得程式碼的靈活性和複用性得以增強,對於容器類的作用更為明顯。
泛型可以加在類、介面、方法之上。如下所示:
public class Generic1<T> {
T t;
List<T> list;
//表示返回值為K,引數型別為K
public <K> K test(K e) {
return e;
}
}
泛型型別引數以<>定義,括號內可以定義多個泛型,如<K,V>。
泛型的型別引數只能是物件型別(包括自定義類),不能是簡單型別
定義了泛型之後,我們就可以使用了。
public class Generic1<T> {
T t;
public <K> K test(K e) {
return e;
}
public static void main(String[] args) {
Generic1<String> g = new Generic1<> ();
System.out.println(g.test(2));
System.out.println(g.test("2"));
}
}
可以看到,我們只需要在使用的時候指定具體的型別即可。我們可以給test方法傳遞任意型別的引數,在沒有泛型前,我們只能用方法過載實現。
型別上界
在上面的例子中,我們可以給類傳遞任何泛型引數。
如果我們有這樣一個需求,傳遞的引數要是某個類的子類。
比如現在有一個類,表示將傳進來的水果製成果汁,那傳進來的類只能是某種水果,而不能是其它東西。extends可以實現這樣的效果。
extends 關鍵字指定泛型型別的上界,表示該型別必須是繼承某個類,或者實現某個介面,也可以是這個類或介面本身。
示例如下:
public class Generic1<T extends List<String>> {
T t;
List<T> list;
public <K extends Number> K test(K e) {
return e;
}
public static void main(String[] args) {
Generic1<ArrayList<String>> g = new Generic1<>();
System.out.println(g.test((byte) 2));
System.out.println(g.test(2));
System.out.println(2L);
System.out.println(g.test(2.0f));
System.out.println(g.test(2.0));
//無法編譯,提示引數型別錯誤
//System.out.println(g.test("hello"));
}
}
在這個例子中, Generic1 類上的泛型引數只能接受 List 或List的子類,傳遞給 test( ) 方法的只能是 Number 型別的資料。
當沒有指定泛型繼承的型別或介面時,預設為 extends Object,此時任何型別都可以作為引數傳入。
注意:對於? extends的萬用字元限定泛型,我們無法向裡面新增元素(只可以新增null),只能讀取其中的元素。
型別下界
super指定泛型型別的下界,表示引數化的型別可能是所指定的型別,或者是此型別的父型別,直至Object。
示例如下:
class Fruit {}
class Apple extends Fruit {}
class Banna extends Fruit {}
class FujiApple extends Apple {}
public class Generic2 {
public static void test(List<? super FujiApple> list) {
list.add(new FujiApple());
//list.add(new Apple());編譯錯誤
}
}
我們可以向 list 中通過 add( ) 方法新增 FujiApple 類,但卻不能新增 Apple( ) 類。事實上我們只能新增 FujiApple 及其子類,而不能新增它的任意超類。
正確的用法應該是這樣的:
public class Generic2 {
public static void test(List<? super FujiApple> list) {
list.add(new FujiApple());
//list.add(new Apple());編譯錯誤
System.out.println(list);
}
public static void main(String[] args) {
List<? super FujiApple> list = new ArrayList<Apple>();
List<? super FujiApple> list1 = new ArrayList<Fruit>();
//編譯錯誤
//List<? super FujiApple> list2 = new ArrayList<Banana>();
test(list);
test(list1);
}
}
沒錯,就是多型,super 提供了多型支援。
注意:對於 ?super 的萬用字元限定泛型,我們可以讀取其中的元素,但讀取出來的元素會變為 Object 型別。
萬用字元(Wildcards)
?叫做萬用字元,表示任意型別,上面的例子中已經出現了。它與型別引數T的不同點如下:(Java 泛型萬用字元和型別限定)
- T 只有extends一種限定方式,<T extends List>是合法的,<T super List>是不合法的
- ?有extends與super兩種限定方式,即<? extends List> 與<? super List>都是合法的
- T 用於泛型類和泛型方法的定義。?用於泛型方法的呼叫和形參,即下面的用法是不合法的:
public class Generic1<? extends List<String> {
public <? extends List> void test(String t) {
}
}
- T 可用於多重限定,如<T extends A & B>,萬用字元 ?不能進行多重限定
PECS法則
生產者(Producer)使用extends,消費者(Consumer)使用super。
如果需要讀取 T 型別的元素,需要宣告成 <? extends T>,例如 List<? extends Apple>,此時不能往列表中新增元素。
如果需要新增 T 型別的元素,需要宣告成 <?super T>,例如 List<? super Apple>,此時可以向其中新增 Apple 及其子類。從其中取元素的時候,要注意取出元素的型別是Object。
如果需要同時新增和使用,不使用泛型萬用字元。
泛型與陣列
不能建立泛型陣列,下面的語句是無法編譯通過的
ArrayList<String>[] genericArray = new ArrayList<String>[10];
陣列是協變的,即如果A ≤ B,則 f(A) ≤ f(B),舉個例子:
Number[] i = new Integer[10];
因為Integer是Number的子類,所以我們可以將Integer型別陣列賦給Number型別陣列的引用變數。我們自然會想到,泛型是否也可以這樣?如下所示:
ArrayList<Number> list = new ArrayList<Integer>();
事實上,上面這句是無法編譯通過的。
很顯然,泛型不是協變的,泛型具有無關性。正確的使用方法如下:
ArrayList<? extends Number> list = new ArrayList<Integer>();
靜態成員與靜態方法
無法通過類上的泛型定義類的泛型靜態成員變數和靜態方法。
例如,下面的寫法是錯誤的
public class Generic3<T extends List> {
private static T t;
public static T void test(String t) {}
}
類的靜態變數與靜態方法是該類所有示例共享的,如果有兩個例項具體化了不同的引數型別,那此時靜態變數和靜態方法的泛型到底是哪一個呢?因此才有這個限制。但你可以在靜態方法上加泛型,如下:
public static <T> void f(T t) { }
泛型擦除(Type Erasure)
Java中的泛型擦除是指在編譯後的位元組碼檔案中型別資訊被擦除,變為原生型別(raw type),因此在執行期,ArrayList<Integer>與ArrayList<String>就是同一個類。
實際上Java泛型的擦除並不是對所有使用泛型的地方都會擦除的,部分地方會保留泛型資訊。泛型技術相當於Java語言的一顆語法糖,這種實現泛型的方法稱為偽泛型(參考深入理解Java虛擬機器第二版)。
在泛型類被型別擦除的時候,如果型別引數部分沒有指定上限,如 <T> 會被轉譯成普通的 Object 型別,如果指定了上限,則型別引數被替換成型別上限。
例如,下面的例子在編譯期無法通過:
public class Generic4 {
public void test(ArrayList<Integer> list) {
}
public void test(ArrayList<String> list) {
}
}
ArrayList<Integer>與ArrayList<String>編譯後都被擦除了,變成了原生型別ArrayList。
(注:深入理解Java虛擬機器(第二版)所說加返回值後,javac可以編譯通過,經測試,在Java 1.8下無法編譯通過)
我們可以藉助Java的Type介面獲取泛型(Java中的Type詳解)。
看下面一個例子:
class FF<K, V> {}
public class Generic4<K extends Integer, V extends String> extends FF<String, Integer> {
public static void main(String[] args) throws NoSuchFieldException, SecurityException {
Generic4<Integer, String> instance = new Generic4<>();
System.out.println(Arrays.toString(instance.getClass().getTypeParameters()));
System.out.println(instance.getClass().getGenericSuperclass());
System.out.println(Arrays.toString(Generic4.class.getTypeParameters()));
System.out.println(Generic4.class.getGenericSuperclass().getTypeName());
System.out.println("-----------------------------------------------");
Map<Integer, String> map = new HashMap<Integer, String>();
Map<Integer, String> map1 = new HashMap<Integer, String>() {
};
ParameterizedType paraType1 = (ParameterizedType) map.getClass().getGenericSuperclass();
Type[] type1 = paraType1.getActualTypeArguments();
System.out.println(Arrays.toString(type1));
ParameterizedType paraType2 = (ParameterizedType) map1.getClass().getGenericSuperclass();
Type[] type2 = paraType2.getActualTypeArguments();
System.out.println(Arrays.toString(type2));
System.out.println("-----------------------------------------------");
FF<String, Integer> ff = new FF<>();
System.out.println(ff.getClass().getGenericSuperclass());
}
}
輸出結果:
[K, V]
com.test.FF<java.lang.String, java.lang.Integer>
[K, V]
com.test.FF<java.lang.String, java.lang.Integer>
-----------------------------------------------
[K, V]
[class java.lang.Integer, class java.lang.String]
-----------------------------------------------
class java.lang.Object
泛型的幾點結論:
- 如果通過 new 建立了類或者直接通過類似FF.class的形式(我們無法使用FF<String,Integer>.class這樣的形式),我們並不能因此獲得實際的型別變數,通過反射只能得到佔位符的形式。這麼做是要避免在建立泛型例項時而建立新的類,從而避免執行時的過度消耗
- 如果繼承了類A或實現了介面B,並且具體化了A或B中的泛型,那麼可以獲得A或B中的實際的型別變數
- 對於成員變數,我們只能得到與類上的泛型宣告相同的結果
- 對於方法宣告中的泛型,如果沒有指定上限,通過反射返回的是Object型別,否則返回的是我們指定的上限
- 對於方法引數中和方法內部的泛型(方法內部的泛型可以藉助匿名內部類間接獲取泛型),我們無法獲得它的實際的變數型別,只能得到佔位符的形式
泛型與序列化
當序列化一個泛型類,然後反序列化時,會喪失原有的型別資訊。示例如下:
class Serial<T> implements Serializable {
ArrayList<T> list = new ArrayList<>();
public void f(T t) {
list.add(t);
}
}
public class Generic5 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, SecurityException {
Serial<String> serial = new Serial<>();
serial.f("hello");
//序列化物件
FileOutputStream out = new FileOutputStream("e:/ToSerial.txt");
ObjectOutputStream objectToOut = new ObjectOutputStream(out);
objectToOut.writeObject(serial);
//反序列化物件
ObjectInputStream objectToRead = new ObjectInputStream(new FileInputStream("e:/ToSerial.txt"));
Serial<Float> restore = (Serial) objectToRead.readObject();
restore.f(2.0f);
System.out.println(restore.list);
objectToOut.close();
objectToRead.close();
}
}
輸出結果:
[hello, 2.0]
從結果可以看到,反序列化後我們可以將浮點數加入到原本是String型別的list中,說明反序列化後原有的型別限制消失了。