Java 泛型擦除原理
问:请比较深入的谈谈你对 Java 泛型擦除的理解和带来的问题认识?
答:Java 的泛型是伪泛型,因为在编译期间所有的泛型信息都会被擦除掉,譬如 List<Integer> 在运行时仅用一个 List 来表示(所以我们可以通过反射 add 方法来向 Integer 的泛型列表添加字符串,因为编译后都成了 Object),这样做的目的是为了和 Java 1.5 之前版本进行兼容。泛型擦除具体来说就是在编译成字节码时首先进行类型检查,接着进行类型擦除(即所有类型参数都用他们的限定类型替换,包括类、变量和方法,如果类型变量有限定则原始类型就用第一个边界的类型来替换,譬如 class Prd<T extends Comparable & Serializable> {} 的原始类型就是 Comparable),接着如果类型擦除和多态性发生冲突时就在子类中生成桥方法解决,接着如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。
先检查再擦除的类型检查是针对引用的,用引用调用泛型方法就会对这个引用调用的方法进行类型检测而无关它真正引用的对象。可以说这是为了兼容带来的问题,如下:
ArrayList<String> arrayList1 = new ArrayList<String>();
arrayList1.add("123"); //编译通过
arrayList1.add(123); //编译错误
String str1 = arrayList1.get(0); //返回类型是 String
ArrayList<String> arrayList2 = new ArrayList();
arrayList2.add("123"); //编译通过
arrayList2.add(123);//编译错误
String object2 = arrayList2.get(0); //返回类型是 String
ArrayList arrayList3 = new ArrayList<String>();
arrayList3.add("123"); //编译通过
arrayList3.add(123); //编译通过
Object object3 = arrayList3.get(0); //返回类型是 Object
所以说擦除前的类型检查是针对引用的,用这个引用调用泛型方法就会对这个引用调用的方法进行类型检测而无关它真正引用的对象。
先检查再擦除带来的另一个问题就是泛型中参数化类型无法支持继承关系,因为泛型的设计初衷就是为了解决 Object 类型转换的弊端而存在,如果泛型中参数化类型支持继承操作就违背了设计的初衷而继续回到原始的 Object 类型转换弊端。也同样可以说这是为了兼容带来的问题,如下:
ArrayList<Object> arrayList1 = new ArrayList<Object>();
arrayList1.add(new Object());
arrayList1.add(new Object());
ArrayList<String> arrayList2 = arrayList1; //编译错误
ArrayList<String> arrayList3 = new ArrayList<String>();
arrayList3.add("abc");
arrayList3.add(new String());
ArrayList<Object> arrayList4 = arrayList3; //编译错误
ArrayList<String> arrayList5 = new ArrayList<Object>(); //编译错误
ArrayList<Object> arrayList6 = new ArrayList<String>(); //编译错误
之所以这样我们可以从反面来论证,假设编译不报错则当通过 arrayList2 调用 get() 方法取值时返回的是 String 类型的对象(因为类型检测是根据引用来决定的),而实际上存放的是 Object 类型的对象,这样 get 出来就会 ClassCastException 了,所以这违背了泛型的初衷。对于 arrayList4 同样假设编译不报错,当调用 arrayList4 的 get() 方法取出来的 String 变成了 Object 虽然不会出现 ClassCastException,但是依然没有意义啊,泛型出现的原因就是为了解决类型转换的问题,其次如果我们通过 arrayList4 的 add() 方法继续添加对象则可以添加任意类型对象实例,这就会导致我们 get() 时更加懵逼不知道加的是什么类型了,所以怎么说都是个死循环。
擦除带来的另一个问题就是泛型与多态的冲突,其通过子类中生成桥方法解决了多态冲突问题,这个问题的验证也很简单,可以通过下面的例子说明:
class Creater<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
class StringCreater extends Creater<String> {
@Override
public void setValue(String value) {
super.setValue(value);
}
@Override
public String getValue() {
return super.getValue();
}
}
StringCreater stringCreater = new StringCreater();
stringCreater.setValue("abc");
stringCreater.setValue(new Object()); //编译错误
上面代码段的运行情况很诧异吧,按理来说 Creater 类被编译擦除后 setValue 方法的参数应该是 Object 类型了,子类 StringCreater 的 setValue 方法参数类型为 String,看起来父子类的这组方法应该是重载关系,所以调用子类的 setValue 方法添加字符串和 Object 类型参数应该都是合法才对,然而从编译来看子类根本没有继承自父类参数为 Object 类型的 setValue 方法,所以说子类的 setValue 方法是对父类的重写而不是重载(从子类添加 @Override 注解没报错也能说明是重写关系)。关于出现上面现象的原理其实我们通过 javap 看下两个类编译后的本质即可:
通过编译后的字节码我们可以看到 Creater 泛型类在编译后类型被擦除为 Object,而我们子类的本意是进行重写实现多态,可类型擦除后子类就和多态产生了冲突,所以编译后的字节码里就出现了桥方法来实现多态。可以看到桥方法的参数类型都是 Object,也就是说子类中真正覆盖父类方法的是桥方法,而子类 String 参数 setValue、getValue 方法上的 @Oveerride 注解只是个假象,桥方法的内部实现是直接调用了我们自己重写的那两个方法;不过上面的 setValue 方法是为了解决类型擦除与多态之间的冲突生成的桥方法,而 getValue 是一种协变,之所以子类中 Object getValue() 和 String getValue() 方法可以同时存在是虚拟机内部的一种区分(我们自己写的代码是不允许这样的),因为虚拟机内部是通过参数类型和返回类型来确定一个方法签名的,所以编译器为了实现泛型的多态允许自己做这个看起来不合法的实现,实质还是交给了虚拟机去区别。
先检查再擦除带来的另一个问题就是泛型读取时会进行自动类型转换问题,所以如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。
关于这个可以通过 javap 去查看使用 List 的 add、get 方法后的字节码指令,你会发现 checkcast 指令不是在 get 方法里面强转的(虽然 get 方法里面返回值在代码里面是被转换成了 T,实际编译擦除了),而是在调用处强转的。
擦除带来的另一个问题是泛型类型参数不能是基本类型,比如 ArrayList<int> 是不合法的,只有 ArrayList<Integer> 是合法的,因为当类型擦除后 ArrayList 的原始类型是 Object,而 Object 是引用类型而不是基本类型。
擦除带来的另一个问题是无法进行具体泛型参数类型的运行时类型检查,譬如 arrayList instanceof ArrayList<String> 是非法的,Java 对于泛型运行时检查的支持仅限于 arrayList instanceof ArrayList<?> 方式。
擦除带来的另一个问题是我们不能抛出也不能捕获泛型类的对象,因为异常是在运行时捕获和抛出的,而在编译时泛型信息会被擦除掉,擦除后两个 catch 会变成一样的东西。也不能在 catch 子句中使用泛型变量,因为泛型信息在编译时已经替换为原始类型(譬如 catch(T) 在限定符情况下会变为原始类型 Throwable),如果可以在 catch 子句中使用则违背了异常的捕获优先级顺序。