之前的文章,我们介绍了Java泛型的基本使用,这里我们将深入到编译期、虚拟机层面当中去。具体地,将会分析介绍类型擦除、泛型翻译方面的内容

类型擦除

在Java虚拟机JVM中是没有泛型这一说的,Java的泛型在编译期会被去除,即所谓的类型擦除。擦除后类型变量将由raw type原始类型来代替,具体地,如果泛型的类型变量上有类型限定,则第一个限定类型即为raw type,否则raw type为Object


/**
 * 泛型类示例
 * @param <T>
 * @apiNote T用来表示类型变量
 */
@ToString
public class Pair<T> {
    private T first;
    private T second;

    public Pair() {
    }

    public Pair(T first, T second){
        this.first = first;
        this.second = second;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public void setSecond(T second) {
        this.second = second;
    }

    public T getFirst() {
        return first;
    }


    public T getSecond() {
        return second;
    }
}


这里,我们对Pair类的Class文件反编译,由于Pair的类型变量T上没有类型限定,则类型擦除后该类型变量T将由Object代替,从下图红框我们也可以看到该类的first、second属性已经由T替换为Object了;而泛型方法minmax的类型变量T由于类型限定,故将使用第一个限定类型Comparable来作为raw type进行替换


java 判断class与泛型是否相等 java泛型类型推断_类型擦除


泛型翻译

由于Java的类型擦除机制,使得泛型类在编译后可以认为就是一个普通的Java类,那如何保证类型正确呢?比如这里testPair1方法中getFirst、getSecond,由于类型擦除机制的存在,其应该返回的是Object类型,讲道理,不应该允许直接赋给String类型的变量啊


// 测试泛型类
public static void testPair1() {
    Pair<String> pair = new Pair<>();
    pair.setFirst("Bob");
    pair.setSecond("Ed");

    String first = pair.getFirst(); 
    String second = pair.getSecond();

    System.out.println("pair1: " + pair);
    System.out.println("first: " + first);
    System.out.println("second: " + second);
}


其实,道理很简单,虽然字节码中的类型是Object了,但是编译器在调用这些方法的同时,会适当的自动的插入强制转换的指令,我们对该方法进行反编译,即可看到checkcast字节码指令


java 判断class与泛型是否相等 java泛型类型推断_泛型_02


现在我们来介绍另外泛型翻译的情形——Bridge Method 桥方法。这里,提供了一个Pair的继承类


public class NumPair extends Pair<Integer> {
    @Override
    public void setFirst(Integer first) {
        System.out.println("call setFirst Method of NumPair");
    }
}


很明显,NumPair的setFirst方法是重写了父类的setFirst方法。但是由于类型擦除机制的存在,父类的setFirst方法的参数first其类型由T已经变为了Object,这一点从反编译后的结果也可以看出来。那么现在问题就来了,父类的方法签名是setFirst(Object first),而子类中该方法签名是setFirst(Integer first),二者不一样。目前看上去,类型擦除机制好像是与多态发生了冲突


java 判断class与泛型是否相等 java泛型类型推断_Pair_03


我们先测试下,看看类型擦除机制与多态是否存在冲突


public class NumPairTest {

    public static void testNumPair() {
        Pair numPair = new NumPair();
        numPair.setFirst(123);      // 通过多态调用子类重写的方法
    }
}


通过打印输出,我们可以确定子类的setFirst被正确地调用了,咦?多态与类型擦除机制二者之间好像又没有冲突了,而是父类与子类的setFirst方法之间签名确实不一样啊,这到底是怎么回事呢?


java 判断class与泛型是否相等 java泛型类型推断_Pair_04


其实道理很简单,还是老办法,我们对子类的字节码文件进行反编译来一探究竟,从下图可以看出,编译器不仅生成我们所提供的setFirst(Integer first)方法,还帮我们自动生成了一个签名为setFirst(Object first)的新方法(如下图蓝框所示)。其实确实是由于类型擦除的原因,导致我们重写方法与父类方法在编译之后出现了签名不一致的情况。编译器为了解决这个冲突,使得多态特性不被破坏。其会自动生成一个与父类签名一致的方法setFirst(Object first),并在其内部去调用我们期望的setFirst(Integer first)方法。由于这个编译器自动生成的方法,一方面是负责来实际重写父类方法的,另一方面则是为了调用开发者实际提供的重写方法,故其被形象地称之为Bridge Method 桥方法


java 判断class与泛型是否相等 java泛型类型推断_Pair_05


参考文献

  1. Java核心技术·卷I 凯.S.霍斯特曼著