泛型(二)->擦除&擦除带来的问题

泛型(二)->擦除&擦除带来的问题

本篇首先介绍泛型的擦除,然后围绕泛型擦除所带来的问题进行精确打击,话不多说,我们直接开始正文.

文中很多例子都会用到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))}两个桥方法这显然不对.

到此泛型基本说完了,如有疑问欢迎留言.

猜你喜欢

转载自blog.csdn.net/zly921112/article/details/63686936
今日推荐