1. 程式人生 > >泛型擦除

泛型擦除

擦除的現象

當開始深入研究泛型的時,會發現其實有些東西是沒有意義的。例如,我們可以宣告ArrayList.class,但是卻無法宣告ArrayList<Integer>.class
這是因為泛型的擦除機制造成的,考慮以下的情況。

public class ErasedTypeEquivalence {
    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println("(c1 == c2) = " + (c1 == c2));
    }
}

以上程式碼中,表明ArrayList<String>ArrayList<Integer>是同一型別。不同的型別在行為方面肯定不同。例如,如果試著將一個Integer型別放入ArrayList<String>,所得的行為和Integer型別放入ArrayList<Integer>完全不同,但是它們仍然是同一型別。
以下的程式碼是對這個問題的一個補充。

class Frob {
}

class Fnorkle {
}

class Quark<Q> {
}

class Particle<POSITION, MOMENTUM> {
}

public class LostInfomation {
    public static void main(String[] args) {
        List<Frob> list = new ArrayList<>();
        Map<Frob,Fnorkle> map = new HashMap<>();
        Quark<Fnorkle> quark = new Quark<>();
        Particle<Long,Double> p = new Particle<>();
        System.out.println(Arrays.toString(
                list.getClass().getTypeParameters()
        ));
        System.out.println(Arrays.toString(
                map.getClass().getTypeParameters()
        ));
        System.out.println(Arrays.toString(
                quark.getClass().getTypeParameters()
        ));
        System.out.println(Arrays.toString(
                p.getClass().getTypeParameters()
        ));
    }
}
// Outputs
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]

Class.getTypeParameters是返回一個TypeVariable物件陣列,表示泛型宣告所宣告的型別引數。但是上例的輸出也表明了,這個方法獲得的只是做引數佔位符的識別符號。

擦除的概念

在泛型程式碼內部,無法獲得任何有關泛型引數型別的資訊。
Java的泛型是使用擦除來實現的,這就意味著在使用泛型的時候,任何具體的型別資訊都會被擦除。寫程式碼時唯一知道就是在使用一個物件。因此,List<String>List<Integer>

在執行時事實上是相同的型別。這兩種形式都會被擦除成它們的"原生"型別,即List。理解擦除以及應該如何處理它,是在學習Java泛型時候的最大阻礙。

因此,可以獲得型別引數識別符號和泛型型別邊界這樣的資訊,但是卻無法知道用來建立某個特定例項的實際的型別引數。

擦除的邊界

class HasF{
    public void f(){
        System.out.println("HasF.f()");
    }
}

class Manipulator<T> {
    private T obj;

    public Manipulator(T obj) {
        this.obj = obj;
    }

    public void manipulate(){
        //obj.f() compile error
    }
}

public class Manipulation {
    public static void main(String[] args) {
        HasF hf = new HasF();
        Manipulator<HasF> manipulator = new Manipulator<>(hf);
        manipulator.manipulate();
    }
}

由於有了擦除機制,Java編譯器無法將manipulate()必須能夠在obj上呼叫f()這一需求對映到HasF擁有f()這一事實上,為了呼叫f(),我們必須協助泛型類,給定泛型類的邊界,以便告知編譯器只能遵循這個邊界的型別。這裡重用了extends關鍵字。並由於有了邊界,下面的程式碼可以編譯了。

class Manipulator2<T extends HasF> {
    private T obj;

    public Manipulator2(T obj) {
        this.obj = obj;
    }

    public void manipulate(){
        obj.f();
    }
}

上面的程式碼中,邊界<T extentds HasF>宣告T必須具有型別HasF或者從HasF匯出來的型別,因為這個約束,所以可以安全地在obj上呼叫f了。
這裡說泛型的型別引數將擦除到它的第一邊界(泛型可能有多個邊界)。這裡提到了型別引數的擦除,編譯器實際上會把型別引數替換成它的擦除,就像上面的示例那樣,T擦除到了HasF,就像在類的宣告中用HasF替換成T一樣。
如同上文所說,我們可以不使用泛型,直接將T替換回會HasF

class Manipulator3 {
    private HasF obj;

    public Manipulator2(HasF obj) {
        this.obj = obj;
    }

    public void manipulate(){
        obj.f();
    }
}

上面的程式碼也可以像Manipulator2中那樣正常工作。但是這並不意味著帶邊界的泛型是毫無意義的。
只有當希望使用的型別引數比某個具體型別(以及它的所有子型別)更加"泛化"時。也就是說,當希望程式碼能跨多個類工作的時候,使用泛型才有幫助。因此,型別引數和它們在有用的泛型程式碼中的應用,通常比簡單的類替換要更為複雜。。但是也不能因為覺得<T extends HasF>的任何東西都是有缺陷的。
例如,假設某個類有返回T的方法,那麼泛型在這裡就是有用處的,因為泛型可以返回確切的型別。例子如下。

class ReturnGenericType<T extends HasF> {
    private T obj;

    public ReturnGenericType(T obj) {
        this.obj = obj;
    }

    public T getObj() {
        return obj;
    }
}

所以,必須檢視所有的程式碼。並確定它是否"足夠複雜"到必須使用泛型的程