12.2 类型擦除

类型擦除是Java中泛型的实现方式。泛型是在编译器这个层次来实现的。在Java源代码中声明的泛型类型信息,在编译过程中会被擦除,只保留不带类型参数的形式。被擦除的类型信息包括泛型类型和泛型方法声明时的形式类型参数,以及参数化类型中的实际类型信息。经过类型擦除之后,包含泛型类型的代码被转换成不包含泛型类型的代码,相当于回到了泛型被引入之前的形式。Java虚拟机在运行字节代码时并不知道泛型类型的存在。虽然为了反射API的需要,在Java字节代码中包含了与泛型类型相关的信息,但这些信息在字节代码执行时是不被使用的。与泛型相关的类型检查由编译器在编译时进行。

1.基本概念

由于某些类型信息在编译过程中被擦除,因此并不是所有类型在运行时都是可用的。编译器和虚拟机所能区分的类型是不同的:对于编译器来说,List<String>和List<Integer>是不同的类型;而对于虚拟机来说,这两者的类型都是List。在运行时可用的类型被称为可具体化类型(reifiable type)。Java中的可具体化类型包括非泛型类型、所有实际类型都是无界通配符的参数化类型、原始类型、基本类型、元素类型为可具体化类型的数组类型,以及父类型和自身都是可具体化类型的嵌套类型。举例来说,String、List<?>、List、int、String[]和MyClass<?>.Inner都是可具体化类型。

除了实际类型都是无界通配符的参数化类型外,Java泛型实现中的最重要的特点是几乎所有参数化类型都是不可具体化的。如List<String>和List<?extends Number>等类型都是不可具体化的。虚拟机在执行字节代码时只能使用运行时可用的可具体化类型。这使Java中与虚拟机相关的语法特性对于不可具体化的参数化类型是不可用的。以异常处理为例,Java代码运行时的异常捕获和处理是由虚拟机来完成的。因此异常类型必须是可具体化的。任何泛型类型都不能直接或间接继承自Throwable类。Java采用这种做法实现泛型的根本出发点是保持Java平台的兼容性,保证泛型引入之前的字节代码在不经过任何修改的情况下就可以在新版本的虚拟机上运行。因此,Java选择在编译器这个层次来实现泛型,而保持虚拟机不变。对于Java这样一个使用广泛的语言来说,这种兼容性很重要,但也带来了泛型在设计上的不自然和使用方式上的局限性。这是各方面利弊因素权衡的结果。

在类型擦除过程中需要处理形式类型参数和参数化类型中的实际类型。对于形式类型参数,在泛型类型声明中的部分会被直接删除,如ObjectHolder<T>被替换成ObjectHolder;在泛型类型代码中出现的则根据上界替换成具体的类型。如果形式类型参数声明了上界,则声明中最左边的上界作为进行替换的类型;如果没有上界,则使用Object类进行替换。而对于参数化类型的实际类型,它们在代码中的出现会被直接删除。进行这些替换之后,可能会出现代码逻辑不合法的情况,编译器会通过插入适当的强制类型转换代码和生成桥接方法(bridge method)来解决。

以代码清单12-1中的ObjectHolder类为例,经过类型擦除后的形式如代码清单12-3所示,由于形式类型参数T没有上界,T的所有出现被替换成Object类。

代码清单12-3 泛型类型经过类型擦除之后的代码示例


public class ObjectHolder{

private Object obj;

public Object getObject(){

return obj;

}

public void setObject(Object obj){

this.obj=obj;

}

}


代码清单12-2中使用ObjectHolder类的代码,经过类型擦除后的形式如代码清单12-4所示。在类型擦除后,ObjectHolder类中的getObject方法的返回值类型实际上是Object类型,因此需要添加强制类型转换把getObject方法的返回值转换成String类型。这些类型转换操作由编译器自动添加。由于编译器已经确保不允许使用除String类的对象之外的其他对象调用setObject方法,因此这个强制类型转换操作始终是合法的。

代码清单12-4 使用泛型类型的代码在类型擦除之后的示例


ObjectHolder holder=new ObjectHolder();

holder.setObject("Hello");

String str=(String)holder.getObject();


2.桥接方法

当一个类继承某个参数化类或实现参数化接口时,在经过类型擦除之后,可能造成所继承的方法的类型签名发生改变。典型的示例是java.lang.Comparable接口的实现类。代码清单12-5给出的Sequence类实现了Comparable接口并定义了compareTo方法的实现。在Comparable接口中添加了实际类型Sequence,说明Sequence类的对象只有与类型相同的对象进行比较才有意义。因此compareTo方法的参数类型是Sequence类。如果试图在调用compareTo方法时使用除Sequence类及其子类之外的其他类型的对象作为参数,那么会出现编译错误。

代码清单12-5 Comparable接口的实现类


public class Sequence implements Comparable<Sequence>{

private final int sequenceNumber;

public Sequence(int sequenceNumber){

this.sequenceNumber=sequenceNumber;

}

public int compareTo(Sequence sequence){

return Integer.compare(sequenceNumber, sequence.sequenceNumber);

}

}


在经过类型擦除之后,Comparable接口的实际类型“<Sequence>”被删除。Sequence类的声明变成了实现原始的Comparable接口。从接口实现的角度来说,这要求Sequence类中包含一个类型签名为“int compareTo(Object)”的方法,否则Sequence类的实现是不正确的。这是由类型擦除造成的,编译器需要添加相应的方法来确保代码实现的正确性。这些由编译器自动添加的方法被称为桥接方法。

对Sequence类来说,编译器会自动添加一个类型签名为“int compareTo(Object)”的方法。在桥接方法的实现中,只是在进行必要的类型转换之后直接调用Sequence类中定义的类型签名为“int compareTo(Sequence)”的方法。代码清单12-6中给出了桥接方法的内部实现。

代码清单12-6 桥接方法的内部实现


public int compareTo(Object obj){

return this.compareTo((Sequence)obj);

}


虽然自动添加的桥接方法compareTo接受Object类型的参数,但是代码中不能直接使用这个方法。使用除Sequence类及其子类之外的其他类型的对象调用compareTo方法会产生编译错误。由于桥接方法在运行时是可见的,可以通过反射API来查找并调用桥接方法。代码清单12-7给出了使用反射API来调用Sequence类中的桥接方法的示例。通过反射API可以查找出表示桥接方法的java.lang.reflect.Method类的对象。通过Method类的isBridge方法可以判断一个方法是否为桥接方法。当尝试使用错误类型的参数调用该方法时,会抛出ClassCastException异常。

代码清单12-7 使用反射API调用桥接方法


public void invoke(){

try{

Method method=Sequence.class.getMethod("compareTo",new Class<?>[]{Object.class});

method.isBridge();//值为true

Sequence seq1=new Sequence(1);

Sequence seq2=new Sequence(2);

method.invoke(seq1,seq2);

method.invoke(seq1,"Hello");//抛出ClassCastException异常

}catch(Exception e){

e.printStackTrace();

}

}


3.类型擦除对泛型特性的影响

类型擦除机制的存在影响了很多泛型的特性。同一泛型类型的所有实例化形式在运行时的表示形式是相同的。每个泛型类型只对应一份字节代码。虚拟机并不区分同一泛型类型的不同实例化形式。List<String>和List<Integer>类型对于虚拟机来说是相同的,表示的都是List接口。所以无法通过类似“List<String>.class”的形式来获取参数化类型的类对象字面量,而只能使用List.class。在运行时并不存在List<String>类型,只有List类型。

除了实际类型都是无上界通配符外的泛型类型的其他实例化形式,都不能用在instanceof操作符中。例如,除了类似“obj instanceof List<?>”之外的其他使用方式,如“obj instanceof List<String>”和“obj instanceof List<?extends Serializable>”等,都是非法的。这是因为instanceof操作符是根据对象的运行时类型来进行判断的,只对可具体化类型有意义。对于参数化类型来说,只能比较类型擦除之后的类型。在instanceof操作符看来,一个ArrayList<String>类的对象也是ArrayList<Integer>类型的实例。因为这两种参数化类型在类型擦除之后的类型都是ArrayList。如果允许这种行为,开发人员容易产生误解,可能破坏代码中的类型安全,因此,instanceof操作符不允许这样的使用方式。

在泛型类型中定义的静态方法和域是被所有的实例化形式的对象所共享的。代码清单12-8中用不同的实际类型实例化了泛型类StaticField。对于虚拟机来说,静态变量count与类型擦除之后的StaticField类型相对应,与具体的参数化类型无关。在引用泛型类型中定义的静态变量和方法时,直接使用原始类型,不能使用参数化类型。“StaticField<String>.count”是非法的引用方式。

代码清单12-8 泛型类型中的静态域


public class StaticField<T>{

public static int count=0;

public StaticField(){

count++;

}

public static void main(String[]args){

new StaticField<String>();

new StaticField<Integer>();

System.out.println(StaticField.count);//输出为2

}

}


泛型类型声明中的形式类型参数不能出现在任何静态上下文中,包括不能出现在静态域的类型声明中、不能出现在静态方法的声明和实现中、不能出现在静态初始化代码块中,以及不能出现在静态嵌套类型中。静态嵌套类型包括静态嵌套类、嵌套接口和嵌套枚举类型等。类型中的静态上下文是与类型关联在一起的,与类型的实例对象无关。由于泛型类型的不同实例化形式在运行时对应的是同一个类型,在静态上下文中使用形式类型参数并没有意义,反而容易造成开发人员的误解,因此,编译器直接禁止这样的用法。