类型擦除

泛型类的类型擦除

泛型被引入Java语言,旨在提供更严格的编译时类型检查并支持泛型编程。为实现泛型,Java编译器通过类型擦除对以下内容进行处理:

  • 将泛型类型中的所有类型参数替换为其边界,若类型参数未受限制则替换为Object。因此生成的字节码仅包含普通类、接口和方法。
  • 如有必要,插入类型转换以保持类型安全。
  • 生成桥接方法以在扩展泛型类型中保持多态性。
    类型擦除确保参数化类型不会创建新类;因此泛型不会产生运行时开销。

在类型擦除过程中,Java编译器会擦除所有类型参数,并将每个类型参数替换为其第一个约束(若该类型参数是受约束的),或替换为Object(若该类型参数是无约束的)。
考虑以下表示单链表中节点的通用类:

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

由于类型参数 T 是无界的,Java 编译器将其替换为 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; }
    // ...
}

在下面的示例中,泛型 Node 类使用了有界类型参数:

public class Node<T extends Comparable<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; }
    // ...
}

Java编译器将有界类型参数T替换为第一个约束类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; }
    // ...
}

泛型方法的类型擦除

Java编译器还会消除泛型方法参数中的类型参数。考虑以下泛型方法:

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

由于 T 是无界的,Java 编译器将其替换为 Object:

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 <T extends Shape> void draw(T shape) { /* ... */ }

Java编译器将 T 替换为 Shape

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

类型擦除和桥接方法的影响

有时类型擦除会导致你可能未曾预料的情况。以下示例展示了这种情况如何发生。该示例说明了编译器在类型擦除过程中如何创建合成方法(称为桥接方法)。
给定以下两个类:

public class Node<T> {

    public T data;

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

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

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

    public void setData(Integer data) {
        IO.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");     // Causes a ClassCastException to be thrown.
Integer x = mn.data;    

类型擦除后代码变成了下面这样:

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

下一节将解释为何在 n.setData(“Hello”); 语句处抛出 ClassCastException 异常。

桥接方法

在编译继承参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个合成方法(称为桥接方法),作为类型擦除过程的一部分。通常无需关注桥接方法,但若其出现在堆栈跟踪中,可能会令人困惑。
类型擦除后, Node 类和 MyNode 类变成了下面这样:

public class Node {

    public Object data;

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

    public void setData(Object data) {
        IO.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node {

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

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

类型擦除后,方法签名不匹配;Node.setData(T) 方法变为 Node.setData(Object)。因此,MyNode.setData(Integer) 方法不会覆盖 Node.setData(Object) 方法。

为了解决这个问题并在类型擦除后保持泛型类型的多态性,Java编译器会生成一个桥接方法,以确保子类型能按预期工作。

对于 MyNode 类,编译器为 setData() 生成了以下桥接方法:

class MyNode extends Node {

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

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

    // ...
}

桥接方法 MyNode.setData(object) 委托给原始的 MyNode.setData(Integer) 方法。因此,n.setData(“Hello”); 语句调用的是 MyNode.setData(Object) 方法,由于 “Hello” 无法转换为 Integer 类型,故抛出 ClassCastException 异常。

不可具体化

我们讨论了编译器移除与类型参数和类型实例化参数相关信息的过程。类型擦除会对变长参数(也称为varargs)方法产生影响,当这些方法的varargs形式参数具有不可实例化类型时尤其如此。有关varargs方法的更多信息,请参阅向方法或构造函数传递信息章节中的”任意数量参数”部分。
这里覆盖以下几个议题:

  • 不可具体化类型
  • 堆污染
  • 具有不可实例化形式参数的可变参数方法的潜在漏洞
  • 防止具有不可实例化形式参数的可变参数方法引发警告

这里的几个议题如果对面向对象编程语言的程序设计有一定的了解会更容易理解,我说的是知道如何设计并实现一门面向对象的编程语言。这里不翻译这些议题的内容,因为我没学过如何设计一门面向对象的编程语言。😳