泛型(二)->擦除&擦除带来的问题
本篇首先介绍泛型的擦除,然后围绕泛型擦除所带来的问题进行精确打击,话不多说,我们直接开始正文.
文中很多例子都会用到Pair这个对象,这里统一声明.
public class Pair<T> {
private T first;
private T second;
public Pair() {
first = null;
second = null;
}
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
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;
}
}
泛型擦除
虚拟机中没有泛型类型的对象–所有对象都是普通的类,无论我们什么时候定义的泛型类型,在虚拟机中都自动转换成了一个相应的原始类型.原始类型就是擦除类型变量,并替换为限定符类型(没有限定符用Object替换)后的泛型类型名.文字描述有点绕口,看下例子马上就能明白.
例如Pair<T>
的原始类型如下
public class Pair {
private Object first;
private Object second;
public Pair() {
first = null;
second = null;
}
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public void setFirst(Object first) {
this.first = first;
}
public Object getSecond() {
return second;
}
public void setSecond(Object second) {
this.second = second;
}
}
因为T是无限定变量,所以全部替换成Object,看起来跟我们写的普通类没有什么区别.
程序中我们可以写出不同类别的Pair,比如Pair<String>
又或者Pair<Integer>
,而在虚拟机中擦除泛型后就变成原始类型的Pair了.
接下来我们具体说说泛型擦除的规则
泛型擦除就是类型变量用第一个限定来替换,如果没有给定限定就用Object替换,例如类Pair<T>
中的类型变量没有限定所以用Object替换,下面我们看一个声明了类型变量限定的例子.
public class Interval<T extends Comparable & Serializable> {
private T lower;
private T upper;
public Interval(T first, T second) {
if (first.compareTo(second) > 0){
lower = second;
upper = first;
}else{
lower = first;
upper = second;
}
}
}
原始类型如下
public class Interval{
private Comparable lower;
private Comparable upper;
public Interval(Comparable first, Comparable second) {
}
}
看到这里大家可能有个大胆的想法,把限定改为Interval<T extends Serializable & Comparable>
会发生什么,这样做的话原始类型就用Serializable替换T,而编译器会在需要的时候插入强制类型转换为Comparable,为了提高效率,我们应该把没有方法的接口放在后面.
翻译泛型表达式
当调用泛型方法的时候,如果擦除返回值类型,编译器将强制插入类型转换.例如下面
Pair<Manager> pair = ...;
Manager manager = pair.getFirst();
擦除后getFirst返回值为Object,编译器会自动插入Manager强制类型转换,所以getFirst()方法会执行如下两个指令
- 对原始方法调用getFirst()
- 把返回值Object强转成Manager
擦除所带来的问题
泛型擦除与多态冲突
我们直接看例子
public class DateInterval extends Pair<Date> {
public void setSecond(Date date){
}
}
在擦除后变成
public class DateInterval extends Pair {
public void setSecond(Date date){
}
}
我们可以发现DateInterval中我们写了一个setSecond(Date)方法,并且擦除后我们还从Pair继承了一个setSecond(Obj)方法.那么当我们调用setSecond()的时候会发生什么呢.
是不是发现这跟我们想的不一样啊,明明有两个setSecond但是编译器却只提示了一个,也就是说这里泛型擦除后与多态产生了冲突.在这种情况下编译器会在DateInterval生成一个桥方法public void setSecond(Object second){setSecond((Date)second)}
,具体流程我们慢慢道来.
Pair声明为Pair<Date>
,而我们DateInterval中又写了setSecond(Date)所以我们正常的理解这更应该是重写对吧,同样虚拟机也是这样做的,泛型擦除后将为原始类型的Pair生成一个桥方法public void setSecond(Object second){setSecond((Date)second)}
调用DateInterval的setSecond(Date),这样就满足我们需求了.当然这只是理论,接下来我们通过DateInterval.class文件来证明我们的观点.
通过javap命令我们可以清楚的看到DateInterval中有个两个setSecond()方法,并且setSecond(Obj)方法先把obj强转成Date然后调用setSecond(Date)方法,印证了我们前面的理论是正确的.
当然我们还可能出现这种情况
public class DateInterval extends Pair<Date> {
public Date getSecond() {
return new Date();
}
}
泛型擦除后会有两个方法
public Date getSecond() {}
public Object getSecond(){}
这在java代码中是肯定不可能出现的,但是虚拟机中,用参数类型和返回类型确定一个方法,所以可能出现两个返回值类型不同的方法字节码,并且虚拟机能够正确处理这个情况.
这里稍加总结
- 虚拟机中没有泛型,只有普通的类和方法.
- 所有的参数类型都用它们的类型限定符替换
- 桥方法被合成来保持多态
- 为了保证类型安全,必要时会插入强制类型转换.
泛型使用注意事项
不能用基本数据类型实例化类型参数
不能用基本数据类型替代类型变量,因此没有Pair<int>
,只有Pair<Integer>
,其原因就是因为类型擦除后,Pair类型变量会被替换成Object或者限定符,而它们不能存储int.
运行时类型查询只适用于原始类型
虚拟机中泛型对象都为原始类型,所以运行时查询只能比较原始类型.
像这样将无法通过编译.同样道理,getClass方法总是返回原始类型.
Pair<String> p1 = new Pair();
Pair<Integer> p2 = new Pair();
if(p1.getClass() == p2.getClass())
比较结果为true,因为getClass方法返回的都是Pair.class
不允许创建参数化类型数组
不能创建参数化类型的数组,例如
Pair<String>[] pairs = new Pair<String>[2]; //error
因为泛型擦除后pairs类型为Pair[]
,然后我们可以转换为Object[]
Object[] o = pairs
然后在赋予新的值
o[0] = new Pair<Integer>();
能够通过数组存储检查,但是当我们使用的时候肯定会产生错误,出于这个考虑不允许创建参数化类型数组.
需要注意的是只是不允许创建,而声明为Pair<String>
是被允许的,只是不能实例化new Pair<String>[2]
.
泛型类的静态上下文中类型变量无效
泛型变量属于对应实例的,而静态属于类的根本没法使用.下面例子是错误的
public class MyInstance<T> {
private static T t;//error
private static T getInstance(){//error
if(t == null){
}
return t;
}
}
擦除后的冲突
这个其实是上面泛型擦除后与多态冲突的另一种情况,我们把上面那个例子在改造下
public class DateInterval extends Pair<Date> {
public void setSecond(Object obj){
}
}
看了这个是不是感觉在故意搞事情
没错我们就是要搞到底,这样当泛型擦除后会出现两个public void setSecond(Object obj)
,那么解决办法只有一个就是重命名我们冲突的方法.
还有一种情况需要注意.如果两个接口是同一个接口的不同参数化实现,那么一个类或者类型变量不能同时成为这两个接口的子类型,书面表达非常拗口直接看例子.
class Calendar implements Comparable<Calendar>{}
class GregorianCalendar extends Calendar implement Comparable<GregorianCalendar>
GregorianCalendar会实现Comparable<Calendar>
和Comparable<GregorianCalendar>
,这是同一个接口的不同参数化.那么合成的桥方法就可能产生冲突,例如如下情况
class GregorianCalendar extends Calendar implement Comparable<GregorianCalendar>{
public int compareTo(Calendar calendar){}
public int compareTo(GregorianCalendar gregorianCalendar){}
}
我们不可能合成public int compareTo(obj){compareTo((Calendar)(obj))}
和public int compareTo(obj){compareTo((GregorianCalendar)(obj))}
两个桥方法这显然不对.
到此泛型基本说完了,如有疑问欢迎留言.