音乐播放器
scraty's Blog
 
文章 标签
9

Powered by Gridea | Theme: Fog

泛型的类型擦除

本文主要介绍泛型在虚拟机中的实现——使用类型擦除的方法。

类型擦除

Java虚拟机中没有泛型类型对象,所有的类都属于普通类。因此Java编译器使用一个普通的类来实现泛型,这个普通类称为相应泛型的原始类型。例如对于如下泛型程序:

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 T getSecond() {
        return second;
    }
    
    public void setFirst(T newValue) {
        first = newValue;
    }
    public void setSecond(T newValue) {
        second = newValue;
    }
}

它的原始类型如下:

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 Object getSecond() {
        return second;
    }
    
    public void setFirst(Object newValue) {
        first = newValue;
    }
    public void setSecond(Object newValue) {
        second = newValue;
    }
}

可以看到,<T>没有了,类型变量T擦除了,并替换为了Object

这里的T没有任何限定,因此替换为Object。如果这里的T有限定的话,会替换为其限定类型。

类型变量的限定使用如下形式:

<T extends BoundingType>

这里的BoundingType就是限定类型。上面这种写法将T限定为BoundingType的子类型。

TBoundingType可以是类,也可以是接口。因为Java中可以同时实现多个接口,所以这里的限定类型也可以有多个。多个限定类型使用如下形式:

<T extends BoundingType1 & BoundingType2 & BoundingType3 ...>

但是其中最多有一个可以是类,其他的只能是接口。

有多个限定类型时,会使用第一个限定类型进行类型擦除。

例如对于如下例子:

public class Interval<T extends Comparable & Serializable> implements Serializable {
    private T lower;
    private T upper;
    ...
    public Interval(T first, T second) {
        if(first.compareTo(second)) <= 0 {
            lower = first;
            upper = second;
        }
        else {
            lower = second;
            upper = first;
        }
    }
}

它的原始类型如下:

public class Interval implements Serializable {
    private Comparable lower;
    private Comparable upper;
    ...
    public Interval(Comparable first, Comparable second) {
        if(first.compareTo(second)) <= 0 {
            lower = first;
            upper = second;
        }
        else {
            lower = second;
            upper = first;
        }
    }
}

编译器的作用

尽管在虚拟机中,泛型类型实际上是一个普通的类,但是编译器不会让你像使用一个普通的类一样使用它。它会对你的使用加以限制,并自动地帮我们做一些事情,让泛型看起来像一个“泛型”。

这主要有以下两点:

  • 调用一个泛型方法时,如果擦除了返回类型,编译器会插入强制类型转换。
  • 编译器使用桥方法来保证泛型子类的多态性。

关于第一点,对于下面这个语句序列:

Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();

我们知道buddies.getFirst()类型擦除之后返回类型是Object,跟buddy的类型是不一样的。这时候,编译器会自动插入到Employee的强制类型转换。

buddies.getFirst()方法的调用实际上被编译器转换为两条虚拟机指令:

  • 对原始方法Pair.getFirst的调用。
  • 将返回的Object类型强制转换为Employee类型。

关于第二点,对于下面这个类:

class DateInterval extends Pair<LocalDate> {
    public void setSecond(LocalDate second) {
        if(second.compareTo(getFirst()) >= 0)
            super.setSecond();
    }
    ...
}

这里它对于setSecond方法的重写应该是有效的,但是它类型擦除之后会变成:

class DateInterval extends Pair {
    public void setSecond(LocalDate second) {...}
    ...
}

PairsetSecond方法参数是Object类型的,因此上面实际上是在重载方法,而不是在重写,DateInterval中还会有一个从Pair继承的方法:

public void setSecond(Object second)

两个方法是不同的,但是它们不应该是不同的。

这时候,编译器会自动生成一个桥方法

public void setSecond(Object second) {
    setSecond((LocalDate) second);
}

可以看到,编译器用我们编写的方法重写了原来的方法,从而使得我们看上去自己“重写了”原来的方法。

参考资料

书籍

Java核心技术 卷Ⅰ 基础知识(原书第11版) 第8章