泛型被引入到Java语言中,以便在编译时提供更严格的类型检查并支持通用编程,为了实现泛型,Java编译器将类型擦除应用于:编程
Object
替换泛型类型中的全部类型参数,所以,生成的字节码仅包含普通的类、接口和方法。类型擦除确保不为参数化类型建立新类,所以,泛型不会产生运行时开销。segmentfault
在类型擦除过程当中,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
:ui
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编译器还会擦除泛型方法参数中的类型参数,考虑如下泛型方法:this
// 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
替换它:code
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
表达式中,或做为数组中的元素。
当参数化类型的变量引用不是该参数化类型的对象时,会发生堆污染,若是程序执行某些操做,在编译时产生未经检查的警告,则会出现这种状况。若是在编译时(在编译时类型检查规则的限制内)或在运行时,没法验证涉及参数化类型(例如,强制转换或方法调用)的操做的正确性,将生成未经检查的警告,例如,在混合原始类型和参数化类型时,或者在执行未经检查的强制转换时,会发生堆污染。
在正常状况下,当全部代码同时编译时,编译器会发出未经检查的警告,以引发你对潜在堆污染的注意,若是单独编译代码的各个部分,则很难检测到堆污染的潜在风险,若是确保代码在没有警告的状况下编译,则不会发生堆污染。
包含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形式参数的不正确处理而抛出ClassCastException
或其余相似异常,你能够经过向静态和非构造方法声明添加如下注解来阻止编译器为这些类型的varargs方法生成的警告:
@SafeVarargs
@SafeVarargs
注解是方法合约的文档部分,这个注解断言该方法的实现不会不正确地处理varargs形式参数。
尽管不太可取,但经过在方法声明中添加如下内容来抑制此类警告也是可能的:
@SuppressWarnings({"unchecked", "varargs"})
可是,此方法不会抑制从方法的调用地点生成的警告,若是你不熟悉@SuppressWarnings
语法,请参阅注解。