类型擦除
泛型被引入到 Java 语言中以在编译时提供更严格的类型检查,并支持泛型编程。 为了实现泛型,Java 编译器将类型擦除应用于:
- 如果类型参数是无界的,则将泛型类型中的所有类型参数替换为它们的边界或对象。所产生的字节码因此只包含普通的类,接口和方法。
- 如果需要,插入类型转换可以保持类型安全。
- 生成桥接方法以保留扩展泛型类型中的多态性。
类型擦除确保没有为参数化的类型创建新的类; 因此,泛型不会导致运行时开销。
泛型类的擦除
在类型擦除过程中,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编译器用第一个界限类Comparable替换有界的类型参数T
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是无界的,因此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 static void 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 warningn.setData("Hello");Integer x = mn.data; // 报错ClassCastException
类型擦除后,这段代码变成:
MyNode mn = new MyNode(5);Node n = (MyNode)mn; // A raw type - compiler throws an unchecked warningn.setData("Hello");Integer x = (String)mn.data; // Causes a ClassCastException to be thrown.
这是执行代码时发生的情况:
- n.setData("Hello") 方法在 MyNode 上执行
- 在setData(Object)的方法体中,由n引用的对象的数据字段被赋值了一个String。
- 可以通过mn引用访问data,并且期望它是一个整数(因为 MyNode 是一个Node )
- 尝试强转一个字符串为Integer,导致ClassCastException
所以解决上面这个问题的办法就是,在继承泛型类时,也要保证是泛型类。不要出现raw类型。
桥接方法
编译继承了参数化的类或实现参数化接口的类或接口时,编译器可能需要创建一个称为桥接方法的合成方法, 作为类型擦除过程的一部分。你通常不需要担心桥接方法,但是如果它出现在栈记录中,你可能会感到困惑。
类型擦除后,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 data)和MyNode的setData(Integer data)方法不会被重写了。
为了解决这个问题并在类型擦除之后保留泛型类型的多态性,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方法。
非具体化的类型
类型擦除讨论了编译器去除类型参数和类型参数有关的信息的过程。类型擦除与可变参数方法相关,其可变参数形式具有不可确定类型。
具体化的类型,其类型信息在运行时完全可用。这包括原始类型、非泛型类型、raw类型和无界通配符的调用。
非具体化的类型是在编译时通过类型擦除来删除信息的类型 - 调用未定义为无界通配符的泛型类型。 非具体化的类型在运行时没有提供所有的信息。非具体化的类型的例子是List 和 List ; JVM 在运行时无法区分这些类型。在某些情况下,非具体化的类型不能使用:例如,在instanceof表达式中,或作为数组中的元素。
堆污染
当参数化类型的变量引用一个不是参数化类型的对象时,就会发生堆污染。 如果程序在编译时执行了一些导致未经检查的警告的操作,则会出现这种情况。 一个未经检查的警告如果产生,无论是在编译时或运行时,操作涉及一个参数化的类型的正确性就不能验证。例如,在混合raw类型和参数化类型时,或执行未经检查的转换时会发生堆污染。
在正常情况下,当所有代码被同时编译时,编译器会发出一个未经检查的警告,以引起您注意潜在的堆污染。 如果分别编译代码的各个部分,则很难检测到堆污染的潜在风险。如果你确保你的代码编译没有警告,那么不会发生堆污染。
非具体化形式参数的可变参数的潜在缺陷
包含可变参数的泛型方法可能导致堆污染。
考虑下面的 ArrayBuilder 类:
public class ArrayBuilder { public static void 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) { List stringListA = 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
当编译器遇到可变参数方法时,它将可变参数形式的参数转换为数组。但是,Java 编程语言不允许创建参数化类型的数组。 在该方法中 ArrayBuilder.addToList,编译器将可变参数形式参数 T... elements 转换为形式参数 T[] elements,即数组。 但是,由于类型擦除,编译器将可变参数形式参数转换为 Object[] elements。因此,可能有堆污染。
以下语句将可变参数形式的参数 l 指定给 Object 数组 objectArgs:
Object[] objectArray = l;
这一语句可能会引入堆污染。l 可以将与可变参数形式参数的参数化类型匹配的值分配给该变量 objectArray, 并且因此可以被分配给该变量 l。但是,编译器不会在此语句中生成未检查的警告。当它将可变参数 List... l 转化为参数 List[] l时,编译器已经生成了警告。 这个声明是合法的; 该变量 l 具有类型 List[],这是Object[]的子类型。
因此,如果您将 List 任何类型的对象分配给objectArray数组的任何元素,则编译器不会发出警告或错误,如以下语句所示:
objectArray[0] = Arrays.asList(42);
该语句将List对象分配给objectArray数组的第一个元素,该List对象包含一个Integer类型的对象。
假设你使用以下语句调用ArrayBuilder.faultyMethod:
ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
At runtime, the JVM throws a ClassCastException at the following statement:
// ClassCastException thrown hereString s = l[0].get(0);
存储在变量l的第一个数组元素的对象的类型为List,但是此语句期望使用类型为List 的对象。
阻止可变参数使用泛型类型引发的告警
如果你声明了具有参数化类型可变参数的方法,并确保该方法不会引发ClassCastException或其他类似的异常,则可以关掉编译器的告警。关闭的方法是将以下注解添加到静态或非构造方法的方法声明上。
@SafeVarargs
@SafeVarargs注释断言该方法的实现不会不适当地处理可变形式参数。
尽管不太理想,但也可以通过在方法声明中添加以下内容来抑制此类警告:
@SuppressWarnings({"unchecked", "varargs"})
但是,这种方法不能抑制从该方法的调用点生成的警告