Java™ 教程(类型擦除)

32次阅读

共计 6289 个字符,预计需要花费 16 分钟才能阅读完成。

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

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

类型擦除确保不为参数化类型创建新类,因此,泛型不会产生运行时开销。
泛型类型擦除
在类型擦除过程中,Java 编译器将擦除所有类型参数,并在类型参数有界时将其每一个替换为第一个边界,如果类型参数为无界,则替换为 Object。
考虑以下表示单链表中节点的泛型类:
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;}
// …
}
因为类型参数 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<T extends Comparable<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;}
// …
}
Java 编译器将有界类型参数 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 编译器还会擦除泛型方法参数中的类型参数,考虑以下泛型方法:
// Counts the number of occurrences of elem in anArray.
//
public static <T> 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 <T extends Shape> void draw(T shape) {/* … */}
Java 编译器将 T 替换为 Shape:
public static void draw(Shape shape) {/* … */}
类型擦除和桥接方法的影响
有时类型擦除会导致你可能没有预料到的情况,以下示例显示了如何发生这种情况,该示例(在桥接方法中描述)显示了编译器有时如何创建一个称为桥接方法的合成方法,作为类型擦除过程的一部分。
给出以下两个类:
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);
}
}
考虑以下代码:
MyNode mn = new MyNode(5);
Node n = mn; // A raw type – compiler throws an unchecked warning
n.setData(“Hello”);
Integer x = mn.data; // Causes a ClassCastException to be thrown.
类型擦除后,此代码变为:
MyNode mn = new MyNode(5);
Node n = (MyNode)mn; // A raw type – compiler throws an unchecked warning
n.setData(“Hello”);
Integer x = (String)mn.data; // Causes a ClassCastException to be thrown.
以下是代码执行时发生的情况:

n.setData(“Hello”)导致方法 setData(Object)在类 MyNode 的对象上执行(MyNode 类从 Node 继承了 setData(Object))。
在 setData(Object)的方法体中,n 引用的对象的 data 字段被分配给 String。
通过 mn 引用的同一对象的 data 字段可以被访问,并且应该是一个整数(因为 mn 是 MyNode,它是 Node<Integer>)。
尝试将 String 分配给 Integer 会导致 Java 编译器在赋值时插入的转换中出现 ClassCastException。

桥接方法
在编译扩展参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个合成方法,称为桥接方法,作为类型擦除过程的一部分,你通常不需要担心桥接方法,但如果出现在堆栈跟踪中,你可能会感到困惑。
在类型擦除之后,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),MyNode 方法变为 setData(Integer),因此,MyNode 的 setData 方法不会覆盖 Node 的 setData 方法。
为了解决这个问题并在类型擦除后保留泛型类型的多态性,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 方法具有相同的方法签名,委托给原始的 setData 方法。
非具体化类型
类型擦除部分讨论编译器移除与类型参数和类型实参相关的信息的过程,类型擦除的结果与变量参数 (也称为 varargs) 方法有关,该方法的 varargs 形式参数具有非具体化的类型,有关 varargs 方法的更多信息,请参阅将信息传递给方法或构造函数的任意数量的参数部分。
可具体化类型是类型信息在运行时完全可用的类型,这包括基元、非泛型类型、原始类型和无界通配符的调用。
非具体化类型是指在编译时通过类型擦除移除信息的类型,即未定义为无界通配符的泛型类型的调用,非具体化类型在运行时不具有所有可用的信息。非具体化类型的例子有 List<String> 和 List<Number>,JVM 无法在运行时区分这些类型,正如对泛型的限制所示,在某些情况下不能使用非具体化类型:例如,在 instanceof 表达式中,或作为数组中的元素。
堆污染
当参数化类型的变量引用不是该参数化类型的对象时,会发生堆污染,如果程序执行某些操作,在编译时产生未经检查的警告,则会出现这种情况。如果在编译时(在编译时类型检查规则的限制内)或在运行时,无法验证涉及参数化类型(例如,强制转换或方法调用)的操作的正确性,将生成未经检查的警告,例如,在混合原始类型和参数化类型时,或者在执行未经检查的强制转换时,会发生堆污染。
在正常情况下,当所有代码同时编译时,编译器会发出未经检查的警告,以引起你对潜在堆污染的注意,如果单独编译代码的各个部分,则很难检测到堆污染的潜在风险,如果确保代码在没有警告的情况下编译,则不会发生堆污染。
具有非具体化形式参数的 Varargs 方法的潜在漏洞
包含 vararg 输入参数的泛型方法可能会导致堆污染。
考虑以下 ArrayBuilder 类:
public class ArrayBuilder {

public static <T> void addToList (List<T> listArg, T… elements) {
for (T x : elements) {
listArg.add(x);
}
}

public static void faultyMethod(List<String>… 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<String> stringListA = new ArrayList<String>();
List<String> stringListB = new ArrayList<String>();

ArrayBuilder.addToList(stringListA, “Seven”, “Eight”, “Nine”);
ArrayBuilder.addToList(stringListB, “Ten”, “Eleven”, “Twelve”);
List<List<String>> listOfStringLists =
new ArrayList<List<String>>();
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
当编译器遇到 varargs 方法时,它会将 varargs 形式参数转换为数组,但是,Java 编程语言不允许创建参数化类型的数组,在方法 ArrayBuilder.addToList 中,编译器将 varargs 形式参数 T … 元素转换为形式参数 T[]元素,即数组,但是,由于类型擦除,编译器会将 varargs 形式参数转换为 Object[]元素,因此,存在堆污染的可能性。
以下语句将 varargs 形式参数 l 分配给 Object 数组 objectArgs:
Object[] objectArray = l;
这种语句可能会引入堆污染,与 varargs 形式参数 l 的参数化类型匹配的值可以分配给变量 objectArray,因此可以分配给 l,但是,编译器不会在此语句中生成未经检查的警告,编译器在将 varargs 形式参数 List<String> … l 转换为形式参数 List[] l 时已生成警告,此语句有效,变量 l 的类型为 List[],它是 Object[]的子类型。
因此,如果将任何类型的 List 对象分配给 objectArray 数组的任何数组组件,编译器不会发出警告或错误,如下所示:
objectArray[0] = Arrays.asList(42);
此语句使用包含一个 Integer 类型的对象的 List 对象分配 objectArray 数组的第一个数组组件。
假设你使用以下语句调用 ArrayBuilder.faultyMethod:
ArrayBuilder.faultyMethod(Arrays.asList(“Hello!”), Arrays.asList(“World!”));
在运行时,JVM 在以下语句中抛出 ClassCastException:
// ClassCastException thrown here
String s = l[0].get(0);
存储在变量 l 的第一个数组组件中的对象具有 List<Integer> 类型,但此语句需要一个 List<String> 类型的对象。
防止来自使用非具体化的形式参数的 Varargs 方法的警告
如果声明一个具有参数化类型参数的 varargs 方法,并确保方法体不会因为对 varargs 形式参数的不正确处理而抛出 ClassCastException 或其他类似异常,你可以通过向静态和非构造方法声明添加以下注解来阻止编译器为这些类型的 varargs 方法生成的警告:
@SafeVarargs
@SafeVarargs 注解是方法合约的文档部分,这个注解断言该方法的实现不会不正确地处理 varargs 形式参数。
尽管不太可取,但通过在方法声明中添加以下内容来抑制此类警告也是可能的:
@SuppressWarnings({“unchecked”, “varargs”})
但是,此方法不会抑制从方法的调用地点生成的警告,如果你不熟悉 @SuppressWarnings 语法,请参阅注解。

上一篇:泛型通配符使用指南

正文完
 0