Java泛型(二):类型擦除与泛型翻译

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

abstract.jpeg

类型擦除

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 泛型类示例
* @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进行替换

figure 1.jpeg

泛型翻译

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

1
2
3
4
5
6
7
8
9
10
11
12
13
// 测试泛型类
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字节码指令

figure 2.jpeg

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

1
2
3
4
5
6
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),二者不一样。目前看上去,类型擦除机制好像是与多态发生了冲突

figure 3.jpeg

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

1
2
3
4
5
6
7
public class NumPairTest {

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

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

figure 4.jpeg

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

figure 5.jpeg

参考文献

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