Java 泛型的类型擦除

类型擦除

类型擦除简介

Java 中的泛型是伪泛型。

类型擦除就是Java泛型只能用于在编译期间的静态类型检查,然后编译器生成的代码会擦除相应的类型信息
这样到了运行期间实际上JVM根本不知道泛型所代表的具体类型。这样做的目的是因为Java泛型是1.5之后才被引入的,为了保持向下兼容
对于这一点,如果阅读 Java 集合框架的源码,可以发现有些类其实并不支持泛型。

1
2
3
4
5
6
7
8
9
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; }
}

编译器做完相应的类型检查之后,实际上到了运行期间上面这段代码实际上将转换成:

1
2
3
4
5
6
7
8
9
public class Node {
private Object data; //T转换成Object
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
}

这意味着不管我们声明 Node<String> 还是Node<Integer>,到了运行期间,JVM 统统视为Node<Object>。

解决:

1
2
3
4
5
6
7
8
9
10
public class Node<T extends Comparable<T>> { 
//Node<T extends Comparable<T>> 是Comaparable即其子类
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 而不再是默认的 Object 了:

1
2
3
4
5
6
7
8
9
10
public class Node {
private Comparable data;
//将T出现的地方替换成Comparable
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
}

类型擦除带来的问题

  • 在 Java 中不允许创建泛型数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Problem1 {
public static void main(String[] args) {
// List<Integer> [] listsOfArray = new List<Integer>[2];
// compile-time error
/*
解析:
compile-time error,我们站在编译器的角度来考虑这个问题:
先来看一下下面这个例子:
Object[] strings = new String[2];
strings[0] = "hi"; // OK
strings[1] = 100; // An ArrayStoreException is thrown.
字符串数组不能存放整型元素,
而且这样的错误往往要等到代码**运行的时候**才能发现,编译器是无法识别的。

接下来我们再来看一下假设Java支持泛型数组的创建会出现什么后果:
Object[] stringLists = new List<String>[];
// compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>(); // OK
// An ArrayStoreException should be thrown, but the runtime can't detect it.
stringLists[1] = new ArrayList<Integer>();

假设我们支持泛型数组的创建,由于运行时期类型信息已经被擦除,
JVM 实际上根本就不知道 new ArrayList<String>() 和 new ArrayList<Integer>() 的区别。
* */
// 可以使用如下语句创建集合数组
// List<Integer> [] listsOfArray = (List<Integer> [])new Object[2];

Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
//因为存在类型擦除,实际上就是c1和c2使用的是同一个.class文件
System.out.println(c1 == c2); // true
}
}
  • 对于泛型代码,Java 编译器实际上还会偷偷帮我们实现一个 Bridge Method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}
}

看完上面的分析之后,你可能会认为在类型擦除后,编译器会将Node和MyNode变成下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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) {
//TODO:子类中的两个setData()方法是重载关系,不是重写关系;因为参数类型不同
//**要实现多态的话,所调用的方法必须在子类中重写**,
// 也就是说这里是要重写 setData(Object) 方法,来实现多态
System.out.println("MyNode.setData");
super.setData(data);
}
}

实际上 Java 编译器对上面代码自动还做了一个处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyNode extends Node {
//TODO: Bridge Method generated by the compiler
public void setData(Object data) {
setData((Integer) data);
//TODO:setData((Integer) data),这样String无法转换成Integer。
//TODO:所以当编译器提示 unchecked warning 的时候,
//我们不能选择忽略,不然要等到运行期间才能发现异常。
}

public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
  • Java 泛型很大程度上只能提供静态类型检查,然后类型的信息就会被擦除,所以利用类型参数创建实例的做法编译器不会通过
1
2
3
4
public static <E> void append(List<E> list) {
E elem = new E(); // compile-time error
list.add(elem);
}

使用反射解决:

1
2
3
4
5
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance();
// TODO:使用反射创建E类型的实例
list.add(elem);
}
  • 无法对泛型代码直接使用 instanceof 关键字,因为 Java 编译器在生成代码的时候会擦除所有相关泛型的类型信息。

    JVM 在运行时期无法识别出ArrayList<Integer>和ArrayList<String>的之间的区别:

1
2
3
4
5
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // compile-time error
// ...
}
}

ArrayList<Integer>, ArrayList<String>, LinkedList<Character>, … 和上面一样,有这个问题。

使用通配符解决:

1
2
3
4
5
public static void rtti(List<?> list) { //TODO:? 表示非限定通配符
if (list instanceof ArrayList<?>) { // OK; instanceof requires a reifiable type(具体的类型)
// ...
}
}