Java™ 教程(類型擦除)

類型擦除

泛型被引入到Java語言中,以便在編譯時提供更嚴格的類型檢查並支持通用編程,爲了實現泛型,Java編譯器將類型擦除應用於:編程

  • 若是類型參數是無界的,則用它們的邊界或Object替換泛型類型中的全部類型參數,所以,生成的字節碼僅包含普通的類、接口和方法。
  • 若有必要,插入類型轉換以保持類型安全。
  • 生成橋接方法以保留擴展泛型類型中的多態性。

類型擦除確保不爲參數化類型建立新類,所以,泛型不會產生運行時開銷。segmentfault

泛型類型擦除

在類型擦除過程當中,Java編譯器將擦除全部類型參數,並在類型參數有界時將其每個替換爲第一個邊界,若是類型參數爲無界,則替換爲Object數組

考慮如下表示單鏈表中節點的泛型類:安全

public class Node<T> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

由於類型參數T是無界的,因此Java編譯器用Object替換它:編程語言

public class Node {

    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() { return data; }
    // ...
}

在如下示例中,泛型Node類使用有界類型參數:函數

public class Node<T extends Comparable<T>> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

Java編譯器將有界類型參數T替換爲第一個邊界類Comparableui

public class Node {

    private Comparable data;
    private Node next;

    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Comparable getData() { return data; }
    // ...
}

泛型方法擦除

Java編譯器還會擦除泛型方法參數中的類型參數,考慮如下泛型方法:this

// Counts the number of occurrences of elem in anArray.
//
public static <T> int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

由於T是無界的,因此Java編譯器用Object替換它:code

public static int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (Object e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

假設定義瞭如下類:對象

class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }

你能夠編寫一個泛型方法來繪製不一樣的形狀:

public static <T extends Shape> void draw(T shape) { /* ... */ }

Java編譯器將T替換爲Shape

public static void draw(Shape shape) { /* ... */ }

類型擦除和橋接方法的影響

有時類型擦除會致使你可能沒有預料到的狀況,如下示例顯示瞭如何發生這種狀況,該示例(在橋接方法中描述)顯示了編譯器有時如何建立一個稱爲橋接方法的合成方法,做爲類型擦除過程的一部分。

給出如下兩個類:

public class Node<T> {

    public T data;

    public Node(T data) { this.data = data; }

    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

考慮如下代碼:

MyNode mn = new MyNode(5);
Node n = mn;            // A raw type - compiler throws an unchecked warning
n.setData("Hello");     
Integer x = mn.data;    // Causes a ClassCastException to be thrown.

類型擦除後,此代碼變爲:

MyNode mn = new MyNode(5);
Node n = (MyNode)mn;         // A raw type - compiler throws an unchecked warning
n.setData("Hello");
Integer x = (String)mn.data; // Causes a ClassCastException to be thrown.

如下是代碼執行時發生的狀況:

  • n.setData("Hello")致使方法setData(Object)在類MyNode的對象上執行(MyNode類從Node繼承了setData(Object))。
  • setData(Object)的方法體中,n引用的對象的data字段被分配給String
  • 經過mn引用的同一對象的data字段能夠被訪問,而且應該是一個整數(由於mnMyNode,它是Node<Integer>)。
  • 嘗試將String分配給Integer會致使Java編譯器在賦值時插入的轉換中出現ClassCastException

橋接方法

在編譯擴展參數化類或實現參數化接口的類或接口時,編譯器可能須要建立一個合成方法,稱爲橋接方法,做爲類型擦除過程的一部分,你一般不須要擔憂橋接方法,但若是出如今堆棧跟蹤中,你可能會感到困惑。

在類型擦除以後,Node和MyNode類變爲:

public class Node {

    public Object data;

    public Node(Object data) { this.data = data; }

    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node {

    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

在類型擦除以後,方法簽名不匹配,Node方法變爲setData(Object),MyNode方法變爲setData(Integer),所以,MyNodesetData方法不會覆蓋NodesetData方法。

爲了解決這個問題並在類型擦除後保留泛型類型的多態性,Java編譯器生成一個橋接方法以確保子類型按預期工做,對於MyNode類,編譯器爲setData生成如下橋接方法:

class MyNode extends Node {

    // Bridge method generated by the compiler
    //
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

    // ...
}

如你所見,橋接方法與類型擦除後的Node類的setData方法具備相同的方法簽名,委託給原始的setData方法。

非具體化類型

類型擦除部分討論編譯器移除與類型參數和類型實參相關的信息的過程,類型擦除的結果與變量參數(也稱爲varargs)方法有關,該方法的varargs形式參數具備非具體化的類型,有關varargs方法的更多信息,請參閱將信息傳遞給方法或構造函數任意數量的參數部分。

可具體化類型是類型信息在運行時徹底可用的類型,這包括基元、非泛型類型、原始類型和無界通配符的調用。

非具體化類型是指在編譯時經過類型擦除移除信息的類型,即未定義爲無界通配符的泛型類型的調用,非具體化類型在運行時不具備全部可用的信息。非具體化類型的例子有List<String>List<Number>,JVM沒法在運行時區分這些類型,正如對泛型的限制所示,在某些狀況下不能使用非具體化類型:例如,在instanceof表達式中,或做爲數組中的元素。

堆污染

當參數化類型的變量引用不是該參數化類型的對象時,會發生堆污染,若是程序執行某些操做,在編譯時產生未經檢查的警告,則會出現這種狀況。若是在編譯時(在編譯時類型檢查規則的限制內)或在運行時,沒法驗證涉及參數化類型(例如,強制轉換或方法調用)的操做的正確性,將生成未經檢查的警告,例如,在混合原始類型和參數化類型時,或者在執行未經檢查的強制轉換時,會發生堆污染。

在正常狀況下,當全部代碼同時編譯時,編譯器會發出未經檢查的警告,以引發你對潛在堆污染的注意,若是單獨編譯代碼的各個部分,則很難檢測到堆污染的潛在風險,若是確保代碼在沒有警告的狀況下編譯,則不會發生堆污染。

具備非具體化形式參數的Varargs方法的潛在漏洞

包含vararg輸入參數的泛型方法可能會致使堆污染。

考慮如下ArrayBuilder類:

public class ArrayBuilder {

  public static <T> void addToList (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }

  public static void faultyMethod(List<String>... l) {
    Object[] objectArray = l;     // Valid
    objectArray[0] = Arrays.asList(42);
    String s = l[0].get(0);       // ClassCastException thrown here
  }

}

如下示例HeapPollutionExample使用ArrayBuiler類:

public class HeapPollutionExample {

  public static void main(String[] args) {

    List<String> stringListA = new ArrayList<String>();
    List<String> stringListB = new ArrayList<String>();

    ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
    ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");
    List<List<String>> listOfStringLists =
      new ArrayList<List<String>>();
    ArrayBuilder.addToList(listOfStringLists,
      stringListA, stringListB);

    ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
  }
}

編譯時,ArrayBuilder.addToList方法的定義產生如下警告:

warning: [varargs] Possible heap pollution from parameterized vararg type T

當編譯器遇到varargs方法時,它會將varargs形式參數轉換爲數組,可是,Java編程語言不容許建立參數化類型的數組,在方法ArrayBuilder.addToList中,編譯器將varargs形式參數T...元素轉換爲形式參數T[]元素,即數組,可是,因爲類型擦除,編譯器會將varargs形式參數轉換爲Object[]元素,所以,存在堆污染的可能性。

如下語句將varargs形式參數l分配給Object數組objectArgs

Object[] objectArray = l;

這種語句可能會引入堆污染,與varargs形式參數l的參數化類型匹配的值能夠分配給變量objectArray,所以能夠分配給l,可是,編譯器不會在此語句中生成未經檢查的警告,編譯器在將varargs形式參數List<String> ... l轉換爲形式參數List[] l時已生成警告,此語句有效,變量l的類型爲List[],它是Object[]的子類型。

所以,若是將任何類型的List對象分配給objectArray數組的任何數組組件,編譯器不會發出警告或錯誤,以下所示:

objectArray[0] = Arrays.asList(42);

此語句使用包含一個Integer類型的對象的List對象分配objectArray數組的第一個數組組件。

假設你使用如下語句調用ArrayBuilder.faultyMethod

ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));

在運行時,JVM在如下語句中拋出ClassCastException

// ClassCastException thrown here
String s = l[0].get(0);

存儲在變量l的第一個數組組件中的對象具備List<Integer>類型,但此語句須要一個List<String>類型的對象。

防止來自使用非具體化的形式參數的Varargs方法的警告

若是聲明一個具備參數化類型參數的varargs方法,並確保方法體不會由於對varargs形式參數的不正確處理而拋出ClassCastException或其餘相似異常,你能夠經過向靜態和非構造方法聲明添加如下註解來阻止編譯器爲這些類型的varargs方法生成的警告:

@SafeVarargs

@SafeVarargs註解是方法合約的文檔部分,這個註解斷言該方法的實現不會不正確地處理varargs形式參數。

儘管不太可取,但經過在方法聲明中添加如下內容來抑制此類警告也是可能的:

@SuppressWarnings({"unchecked", "varargs"})

可是,此方法不會抑制從方法的調用地點生成的警告,若是你不熟悉@SuppressWarnings語法,請參閱註解


上一篇:泛型通配符使用指南

下一篇:泛型的限制

相關文章
相關標籤/搜索