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
是无边界的,编译器会用 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> {
private T data;
private Node next;
public Node(T data, Node 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
编译器也会擦除泛型方法参数中的类型参数。
public static 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 void draw(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);
}
}
调用代码
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
的参数类型为 Object
的,而 MyNode
需要的是 Integer
,这里会报一个 ClassCastException
的异常。
具体的原因,是因为编译器在编译一个继承自泛型类的子类时,为了方法覆盖的签名匹配,保留泛型类型的多态性,会生成一个桥接方法。
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)
的桥接方法,这也是导致问题的原因.
Type Erasure
相关文章:
泛型:为什么使用泛型与泛型的基本使用
泛型:边界和通配符
泛型:类型擦除