Java -- 泛型中的类型擦除机制介绍(二)
上一篇博文中,我们主要介绍了泛型的一些基本使用方法;现在我们来看看泛型的一些较深的特性。
我们也许会声明这样的一组重载方法,来处理String和Integer类型的集合:
class GenericTest {
public void setList(List<String> list){
System.out.println("setList(List<String> list) be called...");
}
public void setList(List<Integer> list){
System.out.println("setList(List<String> list) be called...");
}
}
按照我们的想法,这里我们实现了一组重载方法,分别对List<String>和List<Integer>两种声明的集合做不同的处理。但很遗憾,这种重载的写法不被编译器所认可,它会给我们报一个编译错误:
Erasure of method setList(List<Integer>) is the same as another method in type GenericTest
什么意思呢?我们知道一个类中肯定不允许出现两个声明一模一样的方法。而这条编译错误指出:setList(List<String> list)方法擦除后的形式和该类中另一个方法一致,这里的另一个方法当然就是指setList(List<Integer> list)。
我们也许会感到奇怪,编译器为什么会任务setList()的两种重载形式是一致的呢?我们不是明确了不同的类型变量吗?要明白这一点,我们就需要弄清楚泛型中的“类型擦除”这一概念。
类型擦除,顾名思义,我们明确的某个类型变量,如上例中的String,在编译阶段被编译器忽略了,我们看一个示例:
List<String> list = new ArrayList<String>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
Class.getTypeParameters()会返回一个TypeVariable对象数组,表示有泛型声明所声明的参数;从该方法描述来看,我们貌似可以得到list对象的参数类型信息(这里期望的是String),但是很遗憾,结果返回的是:
[E]
这样得到的还是一个类型标识符,并不是所期望的具体类型:String。在泛型代码中,我们无法获得任何有关泛型参数类型的信息。从这里我们大概可以猜出,List<Integer>和List<String>经过类型擦除后,在编译器看来是代表相同的类型;既然是类型被擦除,我们可以得出这两种形式都被擦除成了它们的原始类型(原始类型的名字就是删去类型变量后的泛型类型名),即List,这时方法声明完全一致;那么上面的编译错误,也就可以理解了。
在具体讲述类型擦除之前,先介绍一个概念:类型变量的限定,因为泛型类型擦除后的原始类型跟类型变量的限定形式有关。
使用泛型时,我们也许希望对某一类的类型能进行比较操作;那么我们在实例化这种类型时,必须要保证该我们明确的这种类型一定是实现了Comparable<T>接口的。我们怎么在泛型变量中表示这种约束呢?沿用上一篇文章中的一个例子:
class Employee<T extends Comparable<T> & Serializable> {
T employeeA;
T employeeB;
public Employee(T employeeA, T employeeB) {
super();
this.employeeA = employeeA;
this.employeeB = employeeB;
}
public T getEmployeeA() {
return employeeA;
}
public void setEmployeeA(T employeeA) {
this.employeeA = employeeA;
}
public T getEmployeeB() {
return employeeB;
}
public void setEmployeeB(T employeeB) {
this.employeeB = employeeB;
}
}
/**
* Generic class without limited type variable
*
* @author xzm
* @param <T>
* Generic type
*/
class Pair<T> {
private T first;
private T second;
public Pair() {
first = null;
second = null;
}
public Pair(T f, T s) {
first = f;
second = s;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
类Employee<T extends Comparable<T> & Serializable>中的类型变量T进行了额外的条件限定,出现了一条类型变量限定列表:
T extends Comparable<T> & Serializable
意为"T并不是一个任意类型了,它必须是实现了Comparable<T>和Serializable接口的子类"(&用来连接条件限定的各个接口和某个类, ","则是用来类型变量,如"<T, U>")。这样,变量类型T的范围就被限制了,相对来说,它变得更加具体。这时,如果我们在实例化Employee对象时,明确的类型如果没有实现上述限定的两个接口,编译器就会报错。这里只能使用关键字extends,不能使用implements;也许是出于extends关键字更接近子类概念的原因;这样也免于新增一个关键字。注意,类型变量T和条件绑定的类型既可以是类,也可以是接口;这种情景下,也要符合Java单继承、多实现的规范(限定列表中根据需求可以出现多个接口,但只能有一个类)。如果我们用一个类作为条件限定的话,那么这个类一定是位于类型变量限定列表的首位,否则会报编译错误。例如:
class Employee<T extends Pair<T> &Comparable<T> & Serializable>
介绍完类型变量的限定内容后,我们就可以讨论类型擦除后的原生类型这一问题了。类型擦除后的原生类型由编译器决定,跟类型变量的状态有关。这时只有两种情况:类型变量未被限定、类型变量被限定。
如果类型变量未被限定,如Pair的定义,那经过类型擦除后的Pair的原始类型是:
class Pair {
private Object first;
private Object second;
public Pair() {
first = null;
}
public Pair(Object f, Object s) {
first = f;
second = s;
}
public Object getFirst() {
return first;
}
public void setFirst(Object first) {
this.first = first;
}
public Objectget Second() {
return second;
}
public void setSecond(Object second) {
this.second = second;
}
}
对于Employee,原始类型则是:
class Employee {
Comparable employeeA;
Comparable employeeB;
public Employee(Comparable employeeA, Comparable employeeB) {
super();
this.employeeA = employeeA;
this.employeeB = employeeB;
}
public Comparable getEmployeeA() {
return employeeA;
}
public void setEmployeeA(Comparable employeeA) {
this.employeeA = employeeA;
}
public Comparable getEmployeeB() {
return employeeB;
}
public void setEmployeeB(Comparable employeeB) {
this.employeeB = employeeB;
}
}
无论何时定义一个泛型类型,编译器都自动提供了一个相应的原始类型,编译器也只使用原始类型。原始类型用第一个限定的类型变量来替换(来自类型变量限定列表),如果没有给限定就用Object替换。
依据这条规则,我们得出了上面的Employee和Pair的原始类型。一般地,为了提高效率,我们应该将标签接口(没有方法的接口)放在类型变量限定列表的末尾。
至此,我们回头再看:
class GenericTest {
public void setList(List<String> list){
System.out.println("setList(List<String> list) be called...");
}
public void setList(List<Integer> list){
System.out.println("setList(List<String> list) be called...");
}
}
就很好理解编译错误所提示的内容了。
最后我们要记住:“即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性。”
下一篇文章中,我们会根据类型擦除这个特性,来讨论一些泛型表达式和泛型方法调用的细节问题。