Oct 26, 2017
Java 泛型-类型擦除
泛型被引入到 Java 语言中,以便在编译时提供更严格的类型检查,并支持泛型编程。为了落实泛型,Java 编译器使用了类型擦除:
- 使用泛型边界或 Object(如果没有边界)来替换泛型类型中的所有类型参数。因此,产生的字节码只包含普通类、接口和方法。
- 如果需要,插入类型强制转换以保护类型安全。
- 在扩展泛型类型中生成桥梁方法以保存多态性。
类型擦除确保没有为参数化类型创建新类,因此,泛型不会产生运行时开销。
泛型类型的擦除
在类型擦除过程中,Java 编译器会擦除所有的类型参数,如果类型参数是有边界的,则将它的第一个边界来替换每一个类型参数,如果类型参数是无界的,则将 Object 类型替换每一个类型参数。
我们来看下面的这个泛型类:
1 | public class Node<T> { |
因为类型参数 T 是无界的,所以 Java 编译器使用 Object 来代替它:
1 | public class Node { |
在下面的例子中,泛型类 Node 使用一个有边界的泛型参数:
1 | public class Node<T extends Comparable<T>> { |
Java 编译器使用第一个有界类型参数(Comparable)来替换泛型参数 T:
1 | public class Node { |
泛型方法的擦除
当然,Java 编译器也可以在泛型方法的参数中进行类型擦除。我们来看下面的泛型方法吧:
1 | // 计算 anArray 中 elem 的出现次数。 |
因为 T 是无界的,所以 Java 编译器使用 Object 来替换:
1 | // Counts the number of occurrences of elem in anArray. |
假设定义了如下类:
1 | class Shape { /* ... */ } |
你可以写一个泛型方法来描绘不同的形状:
1 | public static <T extends Shape> void draw(T shape) { /* ... */ } |
Java 编译器会使用 Shape 来替换 T:
1 | public static void draw(Shape shape) { /* ... */ } |
类型擦除和桥接方法的作用
有时类型擦除会产生我们预想不到的情况。我们从下面的例子看看它是怎么发生的:
定义下面两个类:
1 | public class Node<T> { |
思考下面的代码:
1 | MyNode mn = new MyNode(5); |
类型擦除后,上面的代码变成如下:
1 | MyNode mn = new MyNode(5); |
桥接方法
当编译一个继承了参数化类或实现了参数化接口的类或接口时,Java 编译器可能需要创建一个称为桥接方法的合成方法,该方法是类型擦除过程的一部分。我们通常不需要担心桥接方法,但是如果出现在堆栈跟踪中,我们可能会感到困惑。
类型擦除后,Node 和 MyNode 类变成了如下:
1 | public class Node { |
我们会发现,类型擦除后,方法的签名并不匹配。 Node 的方法变成了 setData(Object),MyNode 的方法变成了 setData(Integer),所以 MyNode 的 setData 方法没有重写(override) Node 的 setData 方法。
为了解决这个问题并在类型擦除后保留泛型类型的多态性,Java 编译器生成一个桥接方法,以确保子类型按预期工作。
对于 MyNode 类,编译器为 setData 生成以下的桥接方法:
1 | class MyNode extends Node { |
正如我们看到的,桥接方法具有与 Node 类 setData 方法相同的方法签名,在类型擦除后,将其委托给原始的 setData 方法。
不可具体化类型
上面几节我们讨论了编译器擦除类型参数的过程。类型擦除会导致一个结果,这个结果与具有可变形参(也称 varargs)的方法有关,该方法的可变形参有不可具体化类型。
这小节我们讨论如下几个话题:
- 不可具体化类型
- 堆污染
- 具有不可具体化形参的可变参数方法的潜在隐患
- 避免来自不可具体化形参的可变参数方法的警告
不可具体化类型
可具体化类型是指在运行期间可知其类型信息的类型。它包括基本类型、非泛型、原始类型和无界通配符的调用。
不可具体化类型是指编译期间类型信息被类型擦除机制擦除的类型 – 未被定义为无界通配符的泛型类型的调用。一个不可具体化类型在运行时并不知道它的所有信息。例如 List<String>
和 List<Number>
是不可具体化类型,JVM 不能在运行时区别这些类型。
堆污染
当参数化类型的变量引用了非参数化类型的对象时就会发生堆污染。如果程序在编译时执行一些未检查警告的操作,就会发生这样情况。如果在编译时或运行时,调用参数化类型的操作的正确性没有得到验证,就会产生未检查的警告。例如,在混合原始类型和参数化类型时,或在执行未检查类型转化时,会发生堆污染。
具有不可具体化形参的可变参数方法的潜在隐患
包含可变参数的泛型方法会发生堆污染。
思考下面的 ArrayBuilder 类:
1 | public class ArrayBuilder { |
下面的例子中,HeapPollutionExample 使用 ArrayBuilder 类:
1 | public class HeapPollutionExample { |
当编译时,定义的 ArrayBuilder.addToList 方法生成一下警告:
warning: [varargs] Possible heap pollution from parameterized vararg type T
当编译器遇到有可变参数的方法时,它将可变参数转换为数组。然而 Java 不允许创建参数化类型的数组。在 ArrayBuilder.addToList 中,编译器将可变形参 T...
的元素 转化成形参 T[]
的元素。但由于类型擦除,编译器会将 T[]
转化成 Object[]
,这就有可能导致堆污染。
下面的语句将可变参数 l 赋值给 Object 数组 objectArgs:
1 | Object[] objectArray = l; |
这个语句有可能产生堆污染。一个与可变形参 l 的参数化类型不匹配的值可以赋值给变量 objectArgs,当然也可以赋值给 objectArray。然而编译器并不会在这个语句上生成「未检查警告」。实际上,在将可变形参 List<String>... l
转变成形参 List[] l
时,编译器已经生成一个警告。
这个语句是有效的,因为变量 l 具有 List[]
类型,List[]
类型是 Object[]
的子类型。因此,如果我们将任何类型的 List 对象分配给 objectArray 数组的任何数组组件,则编译器不会发出警告或错误:
1 | objectArray[0] = Arrays.asList(42); |
假设我们使用下面的语句来调用 ArrayBuilder.faultyMethod 方法:
1 | ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!")); |
JVM 会在运行时抛出 ClassCastException 异常:
1 | // ClassCastException thrown here |
在变量 l 的第一个数组中存储的对象具有 List<Integer>
类型,但是这个语句期望一个 List<String>
类型的对象。
避免来自不可具体化形参的可变参数方法的警告
如果我们定义了一个具有参数化类型的可变形参方法,要先确保方法本身不会抛出 ClassCastException 异常或因处理不当可变形参造成类似的异常,然后通过添加 @SafeVarargs
注解到静态或非构造方法的声明中,就能避免编译器生成的警告。
@SafeVarargs
注解是方法概要的一部分,该注解断言此方法能恰当地处理可变形参。
也可以通过在方法的声明中添加以下内容来抑制生成警告:
@SuppressWarnings({“unchecked”, “varargs”})
但是这种方法并不会抑制来自方法调用的地方产生警告。