在讨论类型擦除之前,我们必须先来了解一下java的泛型。所谓的泛型就是参数化的类型。这就意思着我们可以具体的类型作为一个参数传递给方法、类、接口。
为什么我们需要泛型呢?首先我们都知道在java里,Object就是对象的父类。Object可以引用任何类型的对象。但是这一点会带来类型安全的问题。而泛型的出现就给java带来了类型安全这一项功能。
- 泛型方法:
class Test {
static <T> void helloworld(T t){}
}
- 泛型类
class Test<T> {
T obj
Test(T obj){
this.obj = obj;
}
}
- 泛型接口
interface Test<T> {
T getData();
}
使用泛型可以提供代码的复用,使用一份代码应用到不同的类型上。其次泛型还保证了类型的安全(在编译期就可以检查出来)。比如说:
ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
// 编译器会阻止下面的操作,从而保证了我们的类型安全
list.add(100); // error
泛型还有一个好处,不需要单独的类型转换,如:
ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
String s0 = (String)list.get(0); // 类型转换是不需要做的
// 下面展示了,我们不需要进行单独的类型转换
String s1 = list.get(0);
String s2 = list.get(1);
前面已经做好铺垫了,我们是时候聊类型擦除的问题了。
什么是类型擦除?所谓的类型擦除是一个只在编译期强制类型约束和在运行期丢弃类型信息的过程。如我们现在有这么一个泛型方法:
public static <T> boolean myequal(T t1, T t2){
return t1.equals(t2);
}
编译器会用Object替换掉类型T,如下:
public static <Object> boolean myequal(Object t1, Object t2){
return t1.equals(t2);
}
泛型类的类型擦除
在类级别的类型擦除遵循这样的规则:首先编译器丢弃类上的类型参数,并用它的第一个绑定类型替换它,如果类型参数没有绑定,就用Object来替换。
- 参数类型没有绑定
public class MyClass<T> {
private T[] elements;
public void doSomething(T element){}
public T getSomething(){}
}
MyClass的类型参数T没有绑定到任何类型,所以将会用Object来替换掉T,替换结果:
public class MyClass {
private Object[] elements;
public void doSomething(Object element){}
public Object getSomething(){}
}
- 参数类型有绑定
interface MyT {}
public class MyClass<T extends MyT> {
private E[] elements;
public void doSomething(T element){}
public T getSomething(){}
}
MyTClass是MyClass的类型参数T第一个绑定到的类型,因此T将会被替换成MyTClass:
public class MyClass {
private MyT[] elements;
public void doSomething(MyT element){}
public MyT getSomething(){}
}
为什么取第一个绑定就OK了呢?比如说,如果MyT还有父类,父类还有父类,那么我们的类型参数就有了很多间接的绑定,而第一个绑定就覆盖了所有的父类,因此用第一个绑定就可以了。
泛型方法的类型擦除
对于泛型方法,它的类型参数不会被存放起来,它遵循这样的规则:首先编译器丢弃方法上的类型参数,并用它的第一个绑定类型替换它,如果类型参数没有绑定,就用Object来替换。
- 参数类型没有绑定
public static <T> void printSomething(T[] arr){
for(T item: arr) {
System.out.printf("%s", item);
}
}
上面的方法,进行类型擦除的结果后:
public static void printSomething(Object[] arr){
for(Object item: arr) {
System.out.printf("%s", item);
}
}
- 参数类型有绑定
public static <T extends MyT> void printSomething(T[] arr){
for(T item: arr) {
System.out.printf("%s", item);
}
}
上面的方法,进行类型擦除的结果后:
public static void printSomething(MyT[] arr){
for(MyT item: arr) {
System.out.printf("%s", item);
}
}
类型擦除中产生的桥接方法
除了上述的规则外,对于那些相似的方法,编译器会创建一些合成方法来区分它们,这个合成方法是扩展相同的第一个绑定类的方法签名,这句话通过下面的例子就会有一个比较直观的认识。
首先我们有这么一个类:
class MyQueue<T> {
private T[] elements;
public MyQueue(int size){
this.elements = (T[])new Object[size];
}
public void add(T data){}
public T dequeue(){
if(elements.length > 0){
return elements[0];
} else {
return null;
}
}
}
上面这个类在类型擦除后,T都会被Object替换(具体规则请参考前面部分)。我们现在写一个类来继承MyQueue:
class IntegerQueue extends MyQueue<Integer> {
public IntegerQueue(int size){
super(size);
}
@Override
public void add(Integer data) {
super.add(data);
}
}
接着我们写一个测试方法来引述出它的合成方法的原理:
我们可以看到MyQueue的add方法的参数类型已变成Object,所以queue.add("Helllo")
是说得过去。IDE也提示了这个类型可能有问题:
其实,这也并不是我们想要的,因为IntegerQueue只想接收Int类型的。那么当我们运行这个测试用例时,我们就看到了下面的错误:
这个例子再次说明泛型是为了类型安全而引入的功能。编译器是怎么做到的呢?它是怎么起作用的呢?
实际上,是编译器额外生成了一个方法(前面提到的合成方法)来做桥接。我们都知道,MyQueue进行类型擦除后,会变成下面这样:
class MyQueue {
private Object[] elements;
public MyQueue(int size){
this.elements = (Object[])new Object[size];
}
public void add(Object data){}
public Object dequeue(){
if(elements.length > 0){
return elements[0];
} else {
return null;
}
}
}
IntegerQueue
的方法public void add(Integer data){}
和 MyQueue
public void add(Object data){}
是相似的,编译器就会为相似的方法创建一个中间的方法来做它们之间的桥。为了保证泛型的多态性在类型擦除,这个方法是生成在IntegerQueue
,而且编译器能够保证这种相似的方法不会匹配错,也就是编译都会这种相似的方法创建一个合成方法在它们之间做桥接。如何上面提到的,编译器创建的桥接方法如下:
static class IntegerQueue extends MyQueue<Integer> {
public void add(Object data){
add((Integer) data);
}
public void add(Integer data) {
super.add(data);
}
}
本例子中的这个合成方法就是
public void add(Object data){
add((Integer) data);
}
它是扩展了相同的第一个绑定类的方法签名,换句话说就是它们的方法名是一样的,而这个合成方法的参数的类型是类型擦除后的第一个绑定类(具体可以参考前面部分的内容)。它的作用就是桥接了IntegerQueue类中的add方法与其父类中的add方法,以此来解决泛型在继承中的类型安全问题。