java泛型系列 | 八、类型擦除

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

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

类型擦除确保不会为参数化类型创建新类; 因此泛型不会产生运行时开销。

1、通用泛型的擦除

在类型擦除过程中,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 编译器用第一个绑定类 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; }
    // ...
}

2、泛型方法的擦除

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) { /* ... */ }

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

    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");     // 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; 陈述。

4、桥接方法(Bridge Methods)

在编译扩展参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个合成方法,称为桥接方法,作为类型擦除过程的一部分。 您通常不需要担心桥接方法,

但如果堆栈跟踪中出现桥接方法,您可能会感到困惑。类型擦除后,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(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) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

桥接方法 MyNode.setData(object) 委托给原始的 MyNode.setData(Integer) 方法。 结果,n.setData("Hello"); 语句调用方法 MyNode.setData(Object),并抛出 ClassCastException,因为“Hello”不能转换为 Integer。

5、非具化类型

类型擦除部分讨论了编译器删除与类型参数和类型参数相关的信息的过程。 类型擦除具有与可变参数( varargs )方法相关的后果,这些方法的可变参数形式参数具有不可具体化的类型。此页面涵盖以下主题:

  • 非具化类型

  • 堆污染

  • 非具化类型形式参数的 Varargs 方法的潜在漏洞

  • 防止来自非具化类型形式参数的 Varargs 方法的警告

1)非具化类型

可具体化的类型是其类型信息在运行时完全可用的类型。 这包括原语、非泛型类型、原始类型和未绑定通配符的调用。

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

2)堆污染

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

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

3)非具化类型形式参数的 Varargs 方法的潜在漏洞

包含可变参数输入参数的通用方法可能会导致堆污染。考虑以下 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 形式参数 分配给 Object 数组 objectArgs:

Object[] objectArray = l;

此语句可能会引入堆污染。 与 varargs 形式参数的参数化类型匹配的值可以分配给变量 objectArray,因此可以分配给 l。 但是编译器不会在此语句中生成未经检

查的警告。 编译器在将 varargs 形参 List<String>... 转换为形参 List[] 时已经产生了警告。 本声明有效; 变量 l 的类型为 List[],它是 Object[] 的子类型。因此,

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

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

此语句将一个包含一个整数类型对象的 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> 类型的对象。

4)防止来自非具化类型形式参数的 Varargs 方法的警告

如果你声明了一个带有参数化类型参数的 varargs 方法,并且你确保方法体不会因为 varargs 形参处理不当而抛出 ClassCastException 或其他类似的异常,

你可以防止编译器发出警告通过将以下注释添加到静态和非构造函数方法声明中,为这些类型的可变参数方法生成:

@SafeVarargs

@SafeVarargs 注释是方法契约的文档部分; 此注释断言该方法的实现不会不正确地处理可变参数形式参数。虽然不太理想,但也可以通过在方法声明中添

加以下内容来抑制此类警告:

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

 

posted @ 2021-09-16 16:38  meow_world  阅读(1031)  评论(0)    收藏  举报