类型擦除

泛型被引入到Java语言中,以便在编译时提供更严格的类型检查并支持泛型编程。为了实现泛型,Java编译器将类型擦除应用于:

  • 如果类型参数是无界的,则用泛型或对象替换泛型类型中的所有类型参数。因此,生成的字节码仅包含普通的类、接口和方法。
  • 如有必要,插入类型铸件以保持类型安全。
  • 生成桥接方法以保留扩展泛型类型中的多态性。

类型擦除能够确保不为参数化类型创建新类,因此,泛型不会产生运行时开销。

擦除泛型类型

在类型擦除过程中,Java编译器将擦除所有类型参数,并在类型参数有界时将其替换为第一个绑定,如果类型参数为无界,则替换为Object。

考虑以下表示单链表中节点的泛型类:

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

因为类型参数T是无界的,所以Java编译器做完相应的类型检查之后,实际上到了运行期间上面这段代码实际上将转换成:

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

在以下示例中,泛型Node类使用有界类型参数:

  1. public class Node<T extends Comparable<T>> {
  2. private T data;
  3. private Node<T> next;
  4. public Node(T data, Node<T> next) {
  5. this.data = data;
  6. this.next = next;
  7. }
  8. public T getData() { return data; }
  9. // ...
  10. }

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

解决:
Java编译器将有界类型参数T替换为第一个绑定类Comparable:

  1. public class Node {
  2. private Comparable data;
  3. private Node next;
  4. public Node(Comparable data, Node next) {
  5. this.data = data;
  6. this.next = next;
  7. }
  8. public Comparable getData() { return data; }
  9. // ...
  10. }

擦除泛型方法

Java编译器还会擦除泛型方法参数中的类型参数。请考虑以下泛型方法:

  1. public static <T> int count(T[] anArray, T elem) {
  2. int cnt = 0;
  3. for (T e : anArray)
  4. if (e.equals(elem))
  5. ++cnt;
  6. return cnt;
  7. }

因为T是无界的,Java编译器将会将它替换为Object:

  1. public static int count(Object[] anArray, Object elem) {
  2. int cnt = 0;
  3. for (Object e : anArray)
  4. if (e.equals(elem))
  5. ++cnt;
  6. return cnt;
  7. }

假设定义了以下类:

  1. class Shape { /* ... */ }
  2. class Circle extends Shape { /* ... */ }
  3. class Rectangle extends Shape { /* ... */ }

可以使用泛型方法绘制不同的图形:

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

Java编译器将会将T替换为Shape:

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

类型擦除带来的问题

  • 在 Java 中不允许创建泛型数组
  1. public class Problem1 {
  2. public static void main(String[] args) {
  3. // List<Integer> [] listsOfArray = new List<Integer>[2];
  4. // compile-time error
  5. /*
  6. 解析:
  7. compile-time error,我们站在编译器的角度来考虑这个问题:
  8. 先来看一下下面这个例子:
  9. Object[] strings = new String[2];
  10. strings[0] = "hi"; // OK
  11. strings[1] = 100; // An ArrayStoreException is thrown.
  12. 字符串数组不能存放整型元素,
  13. 而且这样的错误往往要等到代码**运行的时候**才能发现,编译器是无法识别的。
  14. 接下来我们再来看一下假设Java支持泛型数组的创建会出现什么后果:
  15. Object[] stringLists = new List<String>[];
  16. // compiler error, but pretend it's allowed
  17. stringLists[0] = new ArrayList<String>(); // OK
  18. // An ArrayStoreException should be thrown, but the runtime can't detect it.
  19. stringLists[1] = new ArrayList<Integer>();
  20. 假设我们支持泛型数组的创建,由于运行时期类型信息已经被擦除,
  21. JVM 实际上根本就不知道 new ArrayList<String>() 和 new ArrayList<Integer>() 的区别。
  22. * */
  23. // 可以使用如下语句创建集合数组
  24. // List<Integer> [] listsOfArray = (List<Integer> [])new Object[2];
  25. Class c1 = new ArrayList<String>().getClass();
  26. Class c2 = new ArrayList<Integer>().getClass();
  27. //因为存在类型擦除,实际上就是c1和c2使用的是同一个.class文件
  28. System.out.println(c1 == c2); // true
  29. }
  30. }
  • 对于泛型代码,Java 编译器实际上还会偷偷帮我们实现一个 Bridge Method
  1. public class Node<T> {
  2. public T data;
  3. public Node(T data) { this.data = data; }
  4. public void setData(T data) {
  5. System.out.println("Node.setData");
  6. this.data = data;
  7. }
  8. }
  9. public class MyNode extends Node<Integer> {
  10. public MyNode(Integer data) { super(data); }
  11. public void setData(Integer data) {
  12. System.out.println("MyNode.setData");
  13. super.setData(data);
  14. }
  15. }

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

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

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

  1. public class MyNode extends Node {
  2. //TODO: Bridge Method generated by the compiler
  3. public void setData(Object data) {
  4. setData((Integer) data);
  5. //TODO:setData((Integer) data),这样String无法转换成Integer。
  6. //TODO:所以当编译器提示 unchecked warning 的时候,
  7. //我们不能选择忽略,不然要等到运行期间才能发现异常。
  8. }
  9. public void setData(Integer data) {
  10. System.out.println("MyNode.setData");
  11. super.setData(data);
  12. }
  13. }
  • Java 泛型很大程度上只能提供静态类型检查,然后类型的信息就会被擦除,所以利用类型参数创建实例的做法编译器不会通过
  1. public static <E> void append(List<E> list) {
  2. E elem = new E(); // compile-time error
  3. list.add(elem);
  4. }

使用反射解决:

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

ArrayList, ArrayList, LinkedList, … 和上面一样,有这个问题。
使用通配符解决:

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