在Java的字节码文件中,只存在普通的类、接口或者方法。泛型擦除不会引入新的类,所以不会产生额外的运行时开销Java编译器将会对泛型代码进行以下擦除逻辑:

  • 如果泛型不存在类型边界,则使用Object类替代泛型。
  • 如果泛型存在类型边界,则使用边界的类型替代泛型。
  • 编译器自动加入类型转换代码。
  • 存在继承关系的泛型类,编译器将生成桥接方法,在类型擦除过后依然保留多态特性。


代码示例


       泛型类和泛型方法的擦除逻辑一致,我们以泛型类为例进行说明,不再赘述泛型方法的逻辑。


1. 无类型边界的泛型类的擦除

       我们定义一个泛型测试类:

public class GenericClassErasureTest {

    public static void main(String[] args) {
        Node<String> node = new Node<>("tail", null);
        System.out.println(node.getData());
    }

    public static class Node<T> {
        private T data;
        private Node<T> next;

        public Node(T data, Node<T> next) {
            this.data = data;
            this.next = next;
        }

        public T getData() { return data; }
    }
}

代码清单generic-type-erasure1


       因为没有类型边界,所以编译器会将泛型转换成Object,实际上Node类应该是这样的:

public class Node {

    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() { return data; }
    // ...
}

代码清单generic-type-erasure2


       特别说明:在使用intelliJ IDEA或者JD-GUI等反编译图形工具,查看泛型类字节码时,依然可以看到泛型信息,这于大家所熟悉的“编译之后泛型擦除”的说法相悖。 可以用javap -verbose查看class文件的虚拟机指令。以代码清单generic-type-erasure1的Node类为例,运行javap之后的输出如下所示(仅截取方法描述):

//构造函数
public generic.erasure.GenericClassErasureTest$Node(T, generic.erasure.GenericClassErasureTest$Node<T>);
descriptor: (Ljava/lang/Object;Lgeneric/erasure/GenericClassErasureTest$Node;)V

//getData方法
public T getData();
descriptor: ()Ljava/lang/Object;

       描述符descriptor展示了方法的参数和返回值的类型,参数类型会按照方法声明的顺序展示在括号()内,返回值类型紧跟其后。 如果没有参数,则显示()。如果没有返回值,则显示V。

       可以看到,getData的返回值类型是Ljava/lang/Object,也就是类型为Object,证明了字节码中的泛型会转换成具体的类型,而字节码中的泛型T,则属于具体类型的别名。 事实上,在引入泛型后,字节码新增了Signatures部分,用以记录不属于JVM类型系统的语言级别的类型信息。Signatures的官网说明如下:

Signatures are used to encode Java programming language type information that is not part of the Java Virtual Machine type system, such as generic type and method declarations and parameterized types.

This kind of type information is needed to support reflection and debugging, and by a Java compiler.

       所以,准确来说,在字节码中能够察觉泛型的存在(这也是反编译工具能够还原泛型的原因),但同时泛型也被具体化了,泛型类被编译成了处理具体类型的普通类。


2. 有类型边界的泛型类的擦除

       我们同样定义一个泛型测试类:

public static class UpperBoundTypeClass<T extends String> {
    private T data;

    public UpperBoundTypeClass(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }
}

代码清单generic-type-erasure3


       同样截取方法签名,如下所示:

//getData方法
public T getData();
descriptor: ()Ljava/lang/String;

       可以看到方法的实际返回值是String而非Object。


3. 类型擦除的多态处理——桥接方法

       当编译一个参数化的类或接口的子类或实现类时(以下统称子类),除了类型擦除,编译器还会额外对子类生成桥接方法。 开发者不需要关心桥接方法,但最好还是了解一下编译器的行为,避免被堆栈信息疑惑。考虑如下代码:

public class Node<T> {

    public T data;

    public Node(T data) {
        this.data = data;
    }

    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node<Integer> {
    public MyNode(Integer data) {
        super(data);
    }

    @Override
    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

}

代码清单generic-type-erasure4


       当类型擦除之后,Node类的setData变成setData(Object data),而MyNode的setData方法参数类型为Integer,所以MyNode的setData没有覆盖Node类的setData,未能实现多态。 为了让类型擦除之后的继承关系依然保留多态,编译器为子类新生成了以下方法:

// Bridge method generated by the compiler
public void setData(Object data) {
    setData((Integer) data);
}

代码清单generic-type-erasure5


       在代码清单generic-type-erasure5中,新的setData与Node类的setData拥有相同的方法签名,实现了多态,方法内部通过强制类型转换,再调用了原先的setData(Integer data)。 同样可以用javap验证这一点,以下是MyNode的setData(Object data)的字节码内容的截取片段:

public void setData(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
...
2: checkcast     #6                  // class java/lang/Integer
...


附件

       本博文所展示的源代码由此获得


参考文献

[1] Josh Juneau.泛型:工作原理及其重要性[EB/OL].https://www.oracle.com/technetwork/cn/articles/java/juneau-generics-2255374-zhs.html,2019-07-02.
[2] The Java™ Tutorials - Generics[EB/OL].https://docs.oracle.com/javase/tutorial/java/generics/index.html,2019-07-02.
[3] The Java® Virtual Machine Specification[EB/OL].https://docs.oracle.com/javase/specs/jvms/se10/html/jvms-4.html#jvms-4.3,2019-07-04.