类型擦除

泛型被引入到 Java 语言中以在编译时提供更严格的类型检查,并支持泛型编程。 为了实现泛型,Java 编译器将类型擦除应用于:

  • 如果类型参数是无界的,则将泛型类型中的所有类型参数替换为它们的边界或对象。所产生的字节码因此只包含普通的类,接口和方法。
  • 如果需要,插入类型转换可以保持类型安全。
  • 生成桥接方法以保留扩展泛型类型中的多态性。

类型擦除确保没有为参数化的类型创建新的类; 因此,泛型不会导致运行时开销。

泛型类的擦除

在类型擦除过程中,Java编译器擦除所有类型参数,并在类型参数有界的情况下用它的第一个边界替换每个类型参数; 如果类型参数是无界的,那么它将替换为 Object。

考虑以下泛型类,它表示单向链表中的一个节点:

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

由于参数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> {    private T data;    private Node next;    public Node(T data, Node next) {        this.data = data;        this.next = next;    }    public T getData() {        return data;    }    // ...}

Java编译器用第一个界限类Comparable替换有界的类型参数T

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编译器也会擦除泛型方法参数中的类型参数。考虑下面的泛型方法:

// 计算数组中元素出现的次数public static  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  void draw(T shape) { /* ... */ }

Java编译器将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 warningn.setData("Hello");Integer x = mn.data;    // 报错ClassCastException

类型擦除后,这段代码变成:

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

这是执行代码时发生的情况:

  • n.setData("Hello") 方法在 MyNode 上执行
  • 在setData(Object)的方法体中,由n引用的对象的数据字段被赋值了一个String。
  • 可以通过mn引用访问data,并且期望它是一个整数(因为 MyNode 是一个Node )
  • 尝试强转一个字符串为Integer,导致ClassCastException

所以解决上面这个问题的办法就是,在继承泛型类时,也要保证是泛型类。不要出现raw类型。

桥接方法

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

类型擦除后,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 data)和MyNode的setData(Integer data)方法不会被重写了。

为了解决这个问题并在类型擦除之后保留泛型类型的多态性,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方法。

非具体化的类型

类型擦除讨论了编译器去除类型参数和类型参数有关的信息的过程。类型擦除与可变参数方法相关,其可变参数形式具有不可确定类型。

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

非具体化的类型是在编译时通过类型擦除来删除信息的类型 - 调用未定义为无界通配符的泛型类型。 非具体化的类型在运行时没有提供所有的信息。非具体化的类型的例子是List 和 List ; JVM 在运行时无法区分这些类型。在某些情况下,非具体化的类型不能使用:例如,在instanceof表达式中,或作为数组中的元素。

堆污染

当参数化类型的变量引用一个不是参数化类型的对象时,就会发生堆污染。 如果程序在编译时执行了一些导致未经检查的警告的操作,则会出现这种情况。 一个未经检查的警告如果产生,无论是在编译时或运行时,操作涉及一个参数化的类型的正确性就不能验证。例如,在混合raw类型和参数化类型时,或执行未经检查的转换时会发生堆污染。

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

非具体化形式参数的可变参数的潜在缺陷

包含可变参数的泛型方法可能导致堆污染。

考虑下面的 ArrayBuilder 类:

public class ArrayBuilder {  public static  void addToList (List listArg, T... elements) {    for (T x : elements) {      listArg.add(x);    }  }  public static void faultyMethod(List... 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 stringListA = new ArrayList();    List stringListB = new ArrayList();    ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");    ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");    List> listOfStringLists =      new ArrayList>();    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

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

以下语句将可变参数形式的参数 l 指定给 Object 数组 objectArgs:

Object[] objectArray = l;

这一语句可能会引入堆污染。l 可以将与可变参数形式参数的参数化类型匹配的值分配给该变量 objectArray, 并且因此可以被分配给该变量 l。但是,编译器不会在此语句中生成未检查的警告。当它将可变参数 List... l 转化为参数 List[] l时,编译器已经生成了警告。 这个声明是合法的; 该变量 l 具有类型 List[],这是Object[]的子类型。

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

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

该语句将List对象分配给objectArray数组的第一个元素,该List对象包含一个Integer类型的对象。

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

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

At runtime, the JVM throws a ClassCastException at the following statement:

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

存储在变量l的第一个数组元素的对象的类型为List,但是此语句期望使用类型为List 的对象。

阻止可变参数使用泛型类型引发的告警

如果你声明了具有参数化类型可变参数的方法,并确保该方法不会引发ClassCastException或其他类似的异常,则可以关掉编译器的告警。关闭的方法是将以下注解添加到静态或非构造方法的方法声明上。

@SafeVarargs

@SafeVarargs注释断言该方法的实现不会不适当地处理可变形式参数。

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

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

但是,这种方法不能抑制从该方法的调用点生成的警告




java泛型 字符串转集合_编译器