类型擦除

在 Java5 以后引入泛型,为了以最小的修改成本兼容之前的 Java 版本。所以只通过修改 javac 工具对泛型做了兼容。

在 JVM 看来,有没有泛型是一样的,泛型在转为 class 文件后将被擦除,这也是相同的方法描述符,相同的参数不同的泛型类型不能重载的原因。如下代码,将无法编译通过:

public void a(String s, List<String> list1) {}

public void a(String s, List<Integer> list2) {}

泛型类没有自己的 Class 类对象,不存在List<String>.classList<Integer>.class,只有List.class。泛型类型不能用在异常处理中,对于 JVM 来说,只认识 MyException,不认识MyException<String>MyException<Integer>

泛型限定符擦除

对于不带限定符的泛型集合,进行 add 和 get 时,会将对应的泛型类型擦除,然后替换为 Object。如下代码:

public void testList() {
    ArrayList<Integer> list = new ArrayList<>();
    list.add(0);
    int result = list.get(0);
    return;
}

在进行 javap 对 class 文件进行反编译时,可以发现通过 add 方法对应指令 invokevirtual 调用时对应常量池的索引值的常量方法描述符是java/util/ArrayList.add:(Ljava/lang/Object;)Z(调用方法 add,入参是一个引用类型 Object,返回值是 boolean 类型表示添加成功或失败)。get 方法对应指令 invokevirtual 调用时对应常量池的索引值的常量方法描述符是java/util/ArrayList.get:(I)Ljava/lang/Object;(入参是一个 int,返回值是一个 Object),如下代码:

public void testList();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: iconst_0
      10: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      13: invokevirtual #5                  // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
      16: pop
      17: aload_1
      18: iconst_0
      19: invokevirtual #6                  // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
      22: checkcast     #7                  // class java/lang/Integer
      25: invokevirtual #8                  // Method java/lang/Integer.intValue:()I
      ...

通过字节码也可以了解到 Java 的自动拆箱使用的是包装类的xxxValue()方法。

其实,并不是个泛型类型经过类型擦除后都变为 Object 类型,对于泛型限定符经过类型擦除后所有泛型参数都将变成所限定的继承类。编译器将会选择泛型所能指代的所有类层次最高的那个作为替换泛型的类。如下代码:

class Test<T extends Number> {
  T test(T t) {
    return t;
  }
}
 T test(T);
    descriptor: (Ljava/lang/Number;)Ljava/lang/Number;
    flags:
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: areturn
      LineNumberTable:
        line 5: 0
    Signature: #11                          // (TT;)TT;

方法描述符中的接受者和返回值都是 Number。

LocalVariableTypeTable

在 class 文件结构中,仅有表结构和无符号数两种数据结构,在字段表和方法表结构中都存在对于属性表的引用。下面是来自掘金的缩略图,共 10 部分组成:魔数(Magic Number)、版本号(Minor&Major Version)、常量池(Constant Pool)、类访问标记 (Access Flags)、类索引(This Class)、超类索引(Super Class)、接口表索引(Interfaces)、字段表(Fields)、方法表(Methods)、属性表(Attributes)

无论是字段表还是方法表都存在属性个数和属性集合,对应到属性表里。方法表中对应到属性表的常用属性的 Code,也就是通过 javap 显示出来的方法描述和方法内操作指令。

其中 stack 表示操作数栈大小,操作数栈是一个后进先出的数据结构,当调用一个方法时,总会伴随着一个栈帧的入栈然后出栈。

locals 表示方法执行过程中局部变量表的大小,这里的大小和方法里的变量个数不等价,因为本着节约的思想(class 字节码的紧凑性),如果一个变量的作用域在方法执行过程中就结束了,那这个变量对应的局部变量表位置可以被后面的读入操作数栈的变量复用。

args_size 表示参数个数,如果是静态方法参数个数与 descriptor 括号中描述的一致,如果是非静态方法,会加上一个 this 参数,因为通过这个 this 才能方法对应堆中的对象。

在 Code 属性的属性项集合中存在 Jdk1.5 以后引入泛型后添加的 LocalVariableTypeTable 项,结构与 LocalVariableTable(用于描述栈帧中局部变量表于 Java 源码中定义的变量之间的关系)类似,虽然不是运行时必须属性,但默认会生成到 class 文件中。

在 LocalVariableTypeTable 属性中新增了 Signature 属性,用于表示字段的特征签名。

虽然都说在经过编译器编译后泛型类型就拿不到了,但是有时候通过一些反编译工具反编译后仍然可以拿到具体类型信息,说明 class 文件还是有存储泛型信息的,但进行类加载后可能这部分信息没有使用。

如下代码:

public void testList() {
    ArrayList<Integer> list = new ArrayList<>();
    list.add(0);
    int result = list.get(0);
    return;
}

如果在进行 javac 的时候加上-g参数生成更多的调试信息,然后使用 javap 反编译的时候加上-c -v -l就能查看字节码更多有用的信息。

javac -g A.java
javap -c -v -l A.class

javap 后用过两个方法描述,一个是构造函数<init>,另一个是 testList 方法,省略构造函数后如下:

 public void testList();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/ArrayList
        ....
        28: istore_2
        29: return
      LineNumberTable:
        line 8: 0
        line 9: 8
        line 10: 17
        line 11: 29
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      30     0  this   Lcom/linewell/license/cloud/util/A;
            8      22     1  list   Ljava/util/ArrayList;
           29       1     2 result   I
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8      22     1  list   Ljava/util/ArrayList<Ljava/lang/Integer;>;

可以看到打印的属性有 LineNumberTable、LocalVariableTable、LocalVariableTypeTable。

其中 LineNumberTable 表示字节码指令与 Java 源码的对应关系,LocalVariableTable 表示 Java 源码中的变量与栈帧中局部变量表的对应关系,并标识出每个变量的作用范围。

为了解决泛型识别问题 JCP 组织对虚拟机规范做了修改,引入了 LocalVariableTypeTable 属性,结构与 LocalVariableTable 类似,其中 Signature 的作用是存储一个方法在字节码层面的特征签名,这个属性保存的不是原生类型,而是泛型化的类型。所以,擦除只是把方法的 code 属性中定义的泛型类型擦除了,Code 属性的属性项集合中的 LocalVariableTypeTable 还是存在泛型的,即元数据上保留的泛型是我们能通过反射来获取参数化类型的依据。

参考

最后修改日期: 2020年1月1日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。