1

类型擦除

泛型被引入到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) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node<Integer> {
    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")导致方法setData(Object)在类MyNode的对象上执行(MyNode类从Node继承了setData(Object))。
  • setData(Object)的方法体中,n引用的对象的data字段被分配给String
  • 通过mn引用的同一对象的data字段可以被访问,并且应该是一个整数(因为mnMyNode,它是Node<Integer>)。
  • 尝试将String分配给Integer会导致Java编译器在赋值时插入的转换中出现ClassCastException

桥接方法

在编译扩展参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个合成方法,称为桥接方法,作为类型擦除过程的一部分,你通常不需要担心桥接方法,但如果出现在堆栈跟踪中,你可能会感到困惑。

在类型擦除之后,Node和MyNode类变为:

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);
    }
}

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

为了解决这个问题并在类型擦除后保留泛型类型的多态性,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) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

    // ...
}

如你所见,桥接方法与类型擦除后的Node类的setData方法具有相同的方法签名,委托给原始的setData方法。

非具体化类型

类型擦除部分讨论编译器移除与类型参数和类型实参相关的信息的过程,类型擦除的结果与变量参数(也称为varargs)方法有关,该方法的varargs形式参数具有非具体化的类型,有关varargs方法的更多信息,请参阅将信息传递给方法或构造函数任意数量的参数部分。

可具体化类型是类型信息在运行时完全可用的类型,这包括基元、非泛型类型、原始类型和无界通配符的调用。

非具体化类型是指在编译时通过类型擦除移除信息的类型,即未定义为无界通配符的泛型类型的调用,非具体化类型在运行时不具有所有可用的信息。非具体化类型的例子有List<String>List<Number>,JVM无法在运行时区分这些类型,正如对泛型的限制所示,在某些情况下不能使用非具体化类型:例如,在instanceof表达式中,或作为数组中的元素。

堆污染

当参数化类型的变量引用不是该参数化类型的对象时,会发生堆污染,如果程序执行某些操作,在编译时产生未经检查的警告,则会出现这种情况。如果在编译时(在编译时类型检查规则的限制内)或在运行时,无法验证涉及参数化类型(例如,强制转换或方法调用)的操作的正确性,将生成未经检查的警告,例如,在混合原始类型和参数化类型时,或者在执行未经检查的强制转换时,会发生堆污染。

在正常情况下,当所有代码同时编译时,编译器会发出未经检查的警告,以引起你对潜在堆污染的注意,如果单独编译代码的各个部分,则很难检测到堆污染的潜在风险,如果确保代码在没有警告的情况下编译,则不会发生堆污染。

具有非具体化形式参数的Varargs方法的潜在漏洞

包含vararg输入参数的泛型方法可能会导致堆污染。

考虑以下ArrayBuilder类:

public class ArrayBuilder {

  public static <T> void addToList (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }

  public static void faultyMethod(List<String>... l) {
    Object[] objectArray = l;     // Valid
    objectArray[0] = Arrays.asList(42);
    String s = l[0].get(0);       // ClassCastException thrown here
  }

}

以下示例HeapPollutionExample使用ArrayBuiler类:

public class HeapPollutionExample {

  public static void main(String[] args) {

    List<String> stringListA = new ArrayList<String>();
    List<String> stringListB = new ArrayList<String>();

    ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
    ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");
    List<List<String>> listOfStringLists =
      new ArrayList<List<String>>();
    ArrayBuilder.addToList(listOfStringLists,
      stringListA, stringListB);

    ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
  }
}

编译时,ArrayBuilder.addToList方法的定义产生以下警告:

warning: [varargs] Possible heap pollution from parameterized vararg type T

当编译器遇到varargs方法时,它会将varargs形式参数转换为数组,但是,Java编程语言不允许创建参数化类型的数组,在方法ArrayBuilder.addToList中,编译器将varargs形式参数T...元素转换为形式参数T[]元素,即数组,但是,由于类型擦除,编译器会将varargs形式参数转换为Object[]元素,因此,存在堆污染的可能性。

以下语句将varargs形式参数l分配给Object数组objectArgs

Object[] objectArray = l;

这种语句可能会引入堆污染,与varargs形式参数l的参数化类型匹配的值可以分配给变量objectArray,因此可以分配给l,但是,编译器不会在此语句中生成未经检查的警告,编译器在将varargs形式参数List<String> ... l转换为形式参数List[] l时已生成警告,此语句有效,变量l的类型为List[],它是Object[]的子类型。

因此,如果将任何类型的List对象分配给objectArray数组的任何数组组件,编译器不会发出警告或错误,如下所示:

objectArray[0] = Arrays.asList(42);

此语句使用包含一个Integer类型的对象的List对象分配objectArray数组的第一个数组组件。

假设你使用以下语句调用ArrayBuilder.faultyMethod

ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));

在运行时,JVM在以下语句中抛出ClassCastException

// ClassCastException thrown here
String s = l[0].get(0);

存储在变量l的第一个数组组件中的对象具有List<Integer>类型,但此语句需要一个List<String>类型的对象。

防止来自使用非具体化的形式参数的Varargs方法的警告

如果声明一个具有参数化类型参数的varargs方法,并确保方法体不会因为对varargs形式参数的不正确处理而抛出ClassCastException或其他类似异常,你可以通过向静态和非构造方法声明添加以下注解来阻止编译器为这些类型的varargs方法生成的警告:

@SafeVarargs

@SafeVarargs注解是方法合约的文档部分,这个注解断言该方法的实现不会不正确地处理varargs形式参数。

尽管不太可取,但通过在方法声明中添加以下内容来抑制此类警告也是可能的:

@SuppressWarnings({"unchecked", "varargs"})

但是,此方法不会抑制从方法的调用地点生成的警告,如果你不熟悉@SuppressWarnings语法,请参阅注解


上一篇:泛型通配符使用指南
下一篇:泛型的限制

博弈
2.5k 声望1.5k 粉丝

态度决定一切