類型擦除
泛型被引入到Java語言中,以便在編譯時提供更嚴格的類型檢查并支持通用編程,為了實現泛型,Java編譯器將類型擦除應用于:
如果類型參數是無界的,則用它們的邊界或Object替換泛型類型中的所有類型參數,因此,生成的字節碼僅包含普通的類、接口和方法。
如有必要,插入類型轉換以保持類型安全。
生成橋接方法以保留擴展泛型類型中的多態性。
類型擦除確保不為參數化類型創建新類,因此,泛型不會產生運行時開銷。
泛型類型擦除在類型擦除過程中,Java編譯器將擦除所有類型參數,并在類型參數有界時將其每一個替換為第一個邊界,如果類型參數為無界,則替換為Object。
考慮以下表示單鏈表中節點的泛型類:
public class Node{ private T data; private Node next; public Node(T data, Node 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> { private T data; private Node next; public Node(T data, Node next) { this.data = data; this.next = next; } public T getData() { return data; } // ... }
Java編譯器將有界類型參數T替換為第一個邊界類Comparable:
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編譯器還會擦除泛型方法參數中的類型參數,考慮以下泛型方法:
// Counts the number of occurrences of elem in anArray. // public staticint count(T[] anArray, T elem) { int cnt = 0; for (T e : anArray) if (e.equals(elem)) ++cnt; return cnt; }
因為T是無界的,所以Java編譯器用Object替換它:
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 staticvoid draw(T shape) { /* ... */ }
Java編譯器將T替換為Shape:
public static void draw(Shape shape) { /* ... */ }類型擦除和橋接方法的影響
有時類型擦除會導致你可能沒有預料到的情況,以下示例顯示了如何發生這種情況,該示例(在橋接方法中描述)顯示了編譯器有時如何創建一個稱為橋接方法的合成方法,作為類型擦除過程的一部分。
給出以下兩個類:
public class Node{ 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 { 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字段可以被訪問,并且應該是一個整數(因為mn是MyNode,它是Node
嘗試將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),因此,MyNode的setData方法不會覆蓋Node的setData方法。
為了解決這個問題并在類型擦除后保留泛型類型的多態性,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
當參數化類型的變量引用不是該參數化類型的對象時,會發生堆污染,如果程序執行某些操作,在編譯時產生未經檢查的警告,則會出現這種情況。如果在編譯時(在編譯時類型檢查規則的限制內)或在運行時,無法驗證涉及參數化類型(例如,強制轉換或方法調用)的操作的正確性,將生成未經檢查的警告,例如,在混合原始類型和參數化類型時,或者在執行未經檢查的強制轉換時,會發生堆污染。
在正常情況下,當所有代碼同時編譯時,編譯器會發出未經檢查的警告,以引起你對潛在堆污染的注意,如果多帶帶編譯代碼的各個部分,則很難檢測到堆污染的潛在風險,如果確保代碼在沒有警告的情況下編譯,則不會發生堆污染。
具有非具體化形式參數的Varargs方法的潛在漏洞包含vararg輸入參數的泛型方法可能會導致堆污染。
考慮以下ArrayBuilder類:
public class ArrayBuilder { public staticvoid addToList (List listArg, T... elements) { for (T x : elements) { listArg.add(x); } } public static void faultyMethod(List ... 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) { ListstringListA = new ArrayList (); List stringListB = new ArrayList (); ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine"); ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve"); List > listOfStringLists = new ArrayList
>(); 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
因此,如果將任何類型的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
如果聲明一個具有參數化類型參數的varargs方法,并確保方法體不會因為對varargs形式參數的不正確處理而拋出ClassCastException或其他類似異常,你可以通過向靜態和非構造方法聲明添加以下注解來阻止編譯器為這些類型的varargs方法生成的警告:
@SafeVarargs
@SafeVarargs注解是方法合約的文檔部分,這個注解斷言該方法的實現不會不正確地處理varargs形式參數。
盡管不太可取,但通過在方法聲明中添加以下內容來抑制此類警告也是可能的:
@SuppressWarnings({"unchecked", "varargs"})
但是,此方法不會抑制從方法的調用地點生成的警告,如果你不熟悉@SuppressWarnings語法,請參閱注解。
上一篇:泛型通配符使用指南 下一篇:泛型的限制文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/72954.html
泛型的限制 要有效地使用Java泛型,必須考慮以下限制: 無法使用基元類型實例化泛型類型 無法創建類型參數的實例 無法聲明類型為類型參數的靜態字段 無法對參數化類型使用強制類型轉換或instanceof 無法創建參數化類型的數組 無法創建、捕獲或拋出參數化類型的對象 無法重載將每個重載的形式參數類型擦除為相同原始類型的方法 無法使用基元類型實例化泛型類型 考慮以下參數化類型: class P...
摘要:然而中的泛型使用了類型擦除,所以只是偽泛型。總結本文介紹了泛型的使用,以及類型擦除相關的問題。一般情況下泛型的使用比較簡單,但是某些情況下,尤其是自己編寫使用泛型的類或者方法時要注意類型擦除的問題。 簡介 Java 在 1.5 引入了泛型機制,泛型本質是參數化類型,也就是說變量的類型是一個參數,在使用時再指定為具體類型。泛型可以用于類、接口、方法,通過使用泛型可以使代碼更簡單、安全。然...
博客地址:Java泛型:類型擦除 前情回顧 Java泛型:泛型類、泛型接口和泛型方法 類型擦除 代碼片段一 Class c1 = new ArrayList().getClass(); Class c2 = new ArrayList().getClass(); System.out.println(c1 == c2); /* Output true */ 顯然在平時使用中,ArrayList...
摘要:可以看到,如果我們給泛型類制定了上限,泛型擦除之后就會被替換成類型的上限。相應的,泛型類中定義的方法的類型也是如此。參考語言類型擦除下界通配符和的區別 本篇博客主要介紹了Java類型擦除的定義,詳細的介紹了類型擦除在Java中所出現的場景。 1. 什么是類型擦除 為了讓你們快速的對類型擦除有一個印象,首先舉一個很簡單也很經典的例子。 // 指定泛型為String List list1 ...
閱讀 1886·2021-11-15 11:46
閱讀 1077·2021-10-26 09:49
閱讀 1819·2021-10-14 09:42
閱讀 3374·2021-09-26 09:55
閱讀 827·2019-08-30 13:58
閱讀 1023·2019-08-29 16:40
閱讀 3462·2019-08-26 10:27
閱讀 601·2019-08-23 18:18