Java 泛型擦除

参考

The Java™ Tutorials
http://www.baeldung.com/java-type-erasure

泛型擦除

  • Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
  • Insert type casts if necessary to preserve type safety.
  • Generate bridge methods to preserve polymorphism in extended generic types.

Type erasure ensures that no new classes are created for parameterized types; consequently, generics incur no runtime overhead.

如果泛型参数是有界的, 那么编译之后会替换成边界, 否则的话会替换成Object. 插入必要的转型操作. 对于继承泛型类的类来说, 编译器还会添加桥接方法bridge methods以保持多态. 因为编译过后并没生成新的类, 所以泛型运行时并没有引入额外的花销.

示例

During the type erasure process, the Java compiler erases all type parameters and replaces each with its first bound if the type parameter is bounded, or Object if the type parameter is unbounded.

上面的replaces each with its first bound还有待研究. 个人理解应该是说, 这种情况, 泛型类型被替换成First. 本来想反编译class文件验证一下, 结果发现Java1.8编译后的class文件, 反编译后竟然看到了泛型参数...
关于使用idea自带的反编译工具反编译class文件, 能看到泛型类型的原因, 可以参考以下几篇文章:
知乎这篇写得挺好, 推荐查看.
java 泛型擦除发生在哪个阶段,如何用反编译工具查看泛型擦除后的代码?
java-generic-types-type-erasure
java-type-erasure-why-can-i-see-the-type-when-i-look-at-the-bytecode.
文章最后写了几个反编译的例子. 可到本文最后查看.

无界泛型参数

public class Node {

    private T data;
    private Node next;

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

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

因为是无界, 所以类型参数直接被替换成Object了, 编译后:

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; }
    // ...
}

有界泛型参数

public class Node> {

    private T data;
    private Node next;

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

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

因为有限定边界, 所以直接被替换成Comparable了, 代码如下:

public class Node {

    private Comparable data;
    private Node next;

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

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

泛型方法的类型擦除

无界

// Counts the number of occurrences of elem in anArray.
//
public static  int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

编译后:

public static int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (Object e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

有界

class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }

泛型方法:

public static  void draw(T shape) { /* ... */ }

编译后:

public static void draw(Shape shape) { /* ... */ }

类型擦除的影响以及桥接方法

考虑下面两个类

public class Node {

    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 {
    public MyNode(Integer data) { super(data); }

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

调用情况:

MyNode mn = new MyNode(5);
Node n = mn;            // A raw type - compiler throws an unchecked warning
n.setData("Hello");     
Integer x = mn.data;    // Causes a ClassCastException to be thrown.

类型擦除后的代码:

MyNode mn = new MyNode(5);
Node n = (MyNode)mn;         // A raw type - compiler throws an unchecked warning
n.setData("Hello");
Integer x = (String)mn.data; // Causes a ClassCastException to be thrown.

解析:

  • n.setData("Hello"); causes the method setData(Object) to be executed on the object of class MyNode. (The MyNode class inherited setData(Object) from Node.)
  • In the body of setData(Object), the data field of the object referenced by n is assigned to a String.
  • The data field of that same object, referenced via mn, can be accessed and is expected to be an integer (since mn is a MyNode which is a Node.
  • Trying to assign a String to an Integer causes a ClassCastException from a cast inserted at the assignment by a Java compiler.

桥接方法

经过类型擦除之后, 上面两个类变成以下形式

public class Node {

    public Object data;

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

    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}
public class MyNode extends Node {

    public MyNode(Integer data) { super(data); }

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

可以看到两个类的方法setData其实不满足Override的要求了, 因为参数类型不同了.
为了解决这个问题, 编译器产生一个桥接方法, 用以维持多态:

class MyNode extends Node {

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

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

    // ...
}

使用javap反编译来查看泛型类型擦除的例子

对下面这段话的验证.

During the type erasure process, the Java compiler erases all type parameters and replaces each with its first bound if the type parameter is bounded.
两个接口AB:

package generictopic.typeerasure;
/**
 * Created by xiaofu on 17-11-7.
 */
public interface A {
    void a();
}
package generictopic.typeerasure;

/**
 * Created by xiaofu on 17-11-7.
 */
public interface B {
    void B();
}

FirstBoundA, 实现AB接口, 且A写在前:

package generictopic.typeerasure;

/**
 * Created by xiaofu on 17-11-7.
 */
public class FirstBoundA {

    public void test(T item){
        System.out.println(item);
    }

}

FirstBoundB, 实现AB接口, 且B写在前:

package generictopic.typeerasure;

/**
 * Created by xiaofu on 17-11-7.
 */
public class FirstBoundB  {

    public void test(T item){
        System.out.println(item);
    }

}

编译这几个文件, 然后分别用javap进行反编译.
javap参数
这里为了简单, 直接使用-s参数, 即Prints internal type signatures.
FirstBoundA.class执行反编译操作:

javap -s FirstBoundA.class 

可以看到对于test方法, descriptor部分显示的是Lgenerictopic/typeerasure/A;, 即泛型类型替换成第一个边界A了.

Compiled from "FirstBoundA.java"
public class generictopic.typeerasure.FirstBoundA {
  public generictopic.typeerasure.FirstBoundA();
    descriptor: ()V

  public void test(T);
    descriptor: (Lgenerictopic/typeerasure/A;)V
}

再看对FirstBoundB.class的反编译结果:
可以看出来, 参数被替换为第一个边界B了.

Compiled from "FirstBoundB.java"
public class generictopic.typeerasure.FirstBoundB {
  public generictopic.typeerasure.FirstBoundB();
    descriptor: ()V

  public void test(T);
    descriptor: (Lgenerictopic/typeerasure/B;)V
}

所以当某个泛型参数有多个边界的时候, 编译器的做法是将类型替换为第一个边界. 不过要注意, 如果该类型参数有多个边界, 而且其中有一个是class的话, 一定要写在第一个, 这是语法要求.

你可能感兴趣的:(Java 泛型擦除)