Java 語言引入泛型是為了在編譯時提供更嚴(yán)格的類型檢查,并支持泛型編程。
為了實現(xiàn)泛型,Java編譯器將類型擦除應(yīng)用于:
- 用邊界值替換泛型類型中的所有類型參數(shù),如果是無限邊界的,則使用
Object替換。因此,生成的字節(jié)碼只包含普通類、接口和方法。 - 如果需要,強(qiáng)制類型轉(zhuǎn)換,以確保類型安全。
- 生成橋接方法來保存擴(kuò)展泛型類型中的多態(tài)性。
類型擦除確保不會為參數(shù)化類型創(chuàng)建新類。因此,泛型不會產(chǎn)生運行時開銷。
泛型類擦除
在類型擦除過程中,Java 編譯器擦除所有類型參數(shù),如果類型參數(shù)有界,則用它的第一個邊界替換每個參數(shù),如果沒有邊界則用 Object 替換。
比如下邊這個單向鏈表節(jié)點的泛型類,
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; }
// ...
}
因為類型參數(shù) T 是無邊界的,編譯器會用 Object 替換 T。
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; }
// ...
}
編譯器會把 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 編譯器也會擦除泛型方法參數(shù)中的類型參數(shù)。
public static <T> int count(T[] anArray, T elem) {
int cnt = 0;
for (T e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
因為 T 是無邊界的,所以編譯器會用 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 static <T extends Shape> void draw(T shape) { /* ... */ }
編譯后會變成:
public static void draw(Shape shape) { /* ... */ }
擦除和橋接函數(shù)
有時候因為類型擦除會導(dǎo)致一些意想不到的情況。
下面的例會解釋如何發(fā)生的。這個例子(在橋接方法中描述)向我們展示了編譯器在類型擦除過程中,如何創(chuàng)建一個合成方法(稱為橋接方法)。
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);
}
}
調(diào)用代碼
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);
}
}
MyNode mn = new MyNode(5);
Node n = mn;
n.setData("Hello"); // 編譯沒有報錯,運行時拋出異常
Integer x = mn.data;
編譯后類型擦除后的代碼:
MyNode mn = new MyNode(5);
Node n = (MyNode)mn;
n.setData("Hello");
Integer x = (String)mn.data;
這個異常是怎么造成的呢?
Node 的 setData 的參數(shù)類型為 Object 的,而 MyNode 需要的是 Integer,這里會報一個 ClassCastException 的異常。
具體的原因,是因為編譯器在編譯一個繼承自泛型類的子類時,為了方法覆蓋的簽名匹配,保留泛型類型的多態(tài)性,會生成一個橋接方法。
class MyNode extends Node {
// 編譯器生成的橋接方法
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
// ...
}
在擦除后,Node 的方法變成 setData(Object),MyNode 的方法變成了 setData(Integer),為了覆蓋 Node 的方法,編譯器在 MyNode 生成了一個 public void setData(Object data) 的橋接方法,這也是導(dǎo)致問題的原因.
相關(guān)文章: