资讯专栏INFORMATION COLUMN

Java™ 教程(类型擦除)

zsy888 / 2765人阅读

类型擦除

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

如果类型参数是无界的,则用它们的边界或Object替换泛型类型中的所有类型参数,因此,生成的字节码仅包含普通的类、接口和方法。

如有必要,插入类型转换以保持类型安全。

生成桥接方法以保留扩展泛型类型中的多态性。

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

泛型类型擦除

在类型擦除过程中,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编译器将有界类型参数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  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 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)。

尝试将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方法的更多信息,请参阅将信息传递给方法或构造函数的任意数量的参数部分。

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

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

堆污染

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

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

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

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

考虑以下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

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

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

Object[] objectArray = l;

这种语句可能会引入堆污染,与varargs形式参数l的参数化类型匹配的值可以分配给变量objectArray,因此可以分配给l,但是,编译器不会在此语句中生成未经检查的警告,编译器在将varargs形式参数List ... 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类型,但此语句需要一个List类型的对象。

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

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

@SafeVarargs

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

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

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

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/72954.html

相关文章

  • Java教程(泛型的限制)

    泛型的限制 要有效地使用Java泛型,必须考虑以下限制: 无法使用基元类型实例化泛型类型 无法创建类型参数的实例 无法声明类型为类型参数的静态字段 无法对参数化类型使用强制类型转换或instanceof 无法创建参数化类型的数组 无法创建、捕获或抛出参数化类型的对象 无法重载将每个重载的形式参数类型擦除为相同原始类型的方法 无法使用基元类型实例化泛型类型 考虑以下参数化类型: class P...

    Bowman_han 评论0 收藏0
  • Java 泛型总结(一):基本用法与类型擦除

    摘要:然而中的泛型使用了类型擦除,所以只是伪泛型。总结本文介绍了泛型的使用,以及类型擦除相关的问题。一般情况下泛型的使用比较简单,但是某些情况下,尤其是自己编写使用泛型的类或者方法时要注意类型擦除的问题。 简介 Java 在 1.5 引入了泛型机制,泛型本质是参数化类型,也就是说变量的类型是一个参数,在使用时再指定为具体类型。泛型可以用于类、接口、方法,通过使用泛型可以使代码更简单、安全。然...

    Java_oldboy 评论0 收藏0
  • Java泛型:类型擦除

    博客地址:Java泛型:类型擦除 前情回顾 Java泛型:泛型类、泛型接口和泛型方法 类型擦除 代码片段一 Class c1 = new ArrayList().getClass(); Class c2 = new ArrayList().getClass(); System.out.println(c1 == c2); /* Output true */ 显然在平时使用中,ArrayList...

    Hanks10100 评论0 收藏0
  • 初探Java类型擦除

    摘要:可以看到,如果我们给泛型类制定了上限,泛型擦除之后就会被替换成类型的上限。相应的,泛型类中定义的方法的类型也是如此。参考语言类型擦除下界通配符和的区别 本篇博客主要介绍了Java类型擦除的定义,详细的介绍了类型擦除在Java中所出现的场景。 1. 什么是类型擦除 为了让你们快速的对类型擦除有一个印象,首先举一个很简单也很经典的例子。 // 指定泛型为String List list1 ...

    DevTalking 评论0 收藏0
  • Java系列之泛型

    摘要:总结泛型的类型必须是引用类型,不能是基本类型,泛型的个数可以有多个,可以使用对创建对象时的泛型类型以及方法参数类型进行限制,如使用关键字和对泛型的具体类型进行向下限制或向上限制,最后一点,可以声明泛型数组,但是不能创建泛型数组的实例。 自从 JDK 1.5 提供了泛型概念,泛型使得开发者可以定义较为安全的类型,不至于强制类型转化时出现类型转化异常,在没有反省之前,可以通过 Object...

    MadPecker 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<