1. 程式人生 > >Java 泛型使用與泛型擦除

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中,說明反序列化後原有的型別限制消失了。