您的浏览器过于古老 & 陈旧。为了更好的访问体验, 请 升级你的浏览器
Ready 发布于2013年01月25日 05:08

原创 Java SE 7新特性:改进使用带泛型可变参数的方法时的编译器警告和错误提示

4469 次浏览 读完需要≈ 16 分钟

内容目录

堆污染

大部分参数化类型,例如ArrayList<Number>List<String>,都属于非具体化类型(non-reifiable types)。非具体化类型是指在运行时(runtime)并不完整的类型。在编译时,非具体化类型经过了一个名为“类型擦除”的过程,编译器删除了与类型参数相关的信息。这将保证Java运行库与那些诞生在Java泛型之前的应用程序之间的二进制兼容性。由于在编译时,类型擦除操作删除了来自参数化类型的相关信息,因此它们是不完整的。

一个参数化类型的变量引用一个非参数化类型的对象,将会导致堆污染。这种情况只会在程序执行了一些在编译时会出现未经检查的警告(unchecked warning)的操作时发生。不管是在编译时(符合编译时类型检查规则的约束),还是在运行时,如果无法验证一个表示参数化类型的操作(例如:类型强转、方法调用)是否正确,就会产生一个未经检查的警告。

先看下面一段代码:

List l = new ArrayList<Number>();
List<String> ls = l;       // unchecked warning
l.add(0, new Integer(42)); // another unchecked warning
String s = ls.get(0);      // ClassCastException is thrown

在类型擦除的时候,类型ArrayList<Number>List<String>将会分别变为ArrayListList。 变量ls具有参数化类型List<String>。当变量l引用的类型List被分配给变量ls时,编译器将会产生一个未经检查的警告;编译器无法确定,而且我们也知道Java虚拟机(JVM) 在运行时也无法确定变量l是否引用了类型List<String>,于是堆污染就产生了。

因此,在编译时,编译器在add语句处生成了又一个未经检查的警告。编译器无法确定变量l引用的类型是List<String>还是List<Integer>(或者其他可能产生堆污染的情况)。

不过,编译器不会在get语句处生成一个警告或错误。这个语句是有效的;程序将调用List<String>.get方法并返回一个字符串对象。不过,在运行时,get语句将会抛出一个类型转换异常java.lang.ClassCastException

更详细地说,堆污染发生在静态类型为List<Number>的List对象l被分配给另一个具有不同静态类型List<String>的List对象ls时。不过编译器仍然允许该分配,以便于向后兼容那些不支持泛型的Java SE 版本。由于类型擦除的缘故,类型List<Number>List<String>都会变为List。这样,编译器允许对象l以原始类型List的方式被分配给对象ls

此外,堆污染还发生在l.add方法被调用时。add方法的第二个形式参数的静态类型是String,但该方法被调用时使用的却是一个不同类型(Integer)的实际参数。不过,编译器仍然允许其通过。由于类型擦除的缘故,add方法的第二个形式参数的类型变成了Object,这里的add被定义为List<E>.add(int, E),而Object是Number和String的公共父类。在类型擦除之后,l.add可以添加一个类型为Object的对象,当然也包括它的子类Integer,所以编译器允许其通过编译。

可变参数方法和非具体化的形式参数

我们先来看下面例子中的ArrayBuilder.addToList方法,它是一个可变参数方法,作用是将可变参数elements中类型为T的对象添加到形式参数List listArg中。

import java.util.*;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(new Integer(42));
    String s = l[0].get(0);    // ClassCastException thrown here
  }}import java.util.*;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(stringListA, "Ten", "Eleven", "Twelve");
    List<List<String>> listOfStringLists = new ArrayList<List<String>>();
    ArrayBuilder.addToList(listOfStringLists, stringListA, stringListB);

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

Java SE 7 的编译器将会在ArrayBuilder.addToList方法的定义出产生如下警告:

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

当遇到可变参数的方法时,编译器会将可变参数形式的形式参数转化为一个数组。但是,Java编程语言并不允许创建泛型数组。在方法ArrayBuilder.addToList中,编译器将可变参数形式的形式参数T... elements转化为一个数组T[] elements。但是由于类型擦除的缘故,编译器会将其转换为Object[] elements,这时,堆污染将可能产生。

注意:ArrayBuilder.addToList方法被调用时,Java SE 5和Java SE 6的编译器将会产生一个警告,这个警告是针对类HeapPollutionExample的;它们不会在声明的时候产生一个警告,而Java SE 7在声明和调用时都会产生一个警告(除非你使用了注解@SuppressWarnings忽略了该警告)。当编译器遇到一个带有非具体化可变参数的形式参数的方法时,相对于在方法调用时(产生警告)而言,在方法声明时产生警告的好处在于方法的声明只有一个,而方法调用的地方可能有多个。

方法ArrayBuilder.faultyMethod展示了对于这些方法为什么编译器会给出警告。方法中的第一条语句将可变参数l分配给了Object数组objectArgs:

Object[] objectArray = l;

这条语句可能引入堆污染,一个与可变参数l的参数化类型相匹配的值可以被赋给变量objectArray,从而也赋给变量l。不过,编译器并不会在这条语句处产生一个未经检查的警告, 因为在将可变参数形式的形式参数List<String>... l转化为形式参数List[] l时,编译器已经产生了一个警告。这条语句是有效的,变量l具有Object[]的子类型List[]。

因此,正如下面这条语句所示,如果你将一个任何类型的List对象赋值给objectArray数组中的任何元素,编译器并不会发出一个错误或警告:

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

这条语句将一个Integer类型的List对象赋值给objectArray的第一个元素。

假如你使用下面的语句调用ArrayBuilder.makeArray方法:

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

在运行时,Java虚拟机(JVM)将会在下面这条语句的地方抛出一个类型转换异常(java.lang.ClassCastException):

String s = l[0].get(0);    // 此处抛出异常ClassCastException

变量l中的第一个元素具有的类型为List<Integer>,但是这条语句期望的是一个具备List<String>类型的对象。

忽略非具体化形式参数的可变参数方法的警告信息

如果你声明了一个具有参数化类型的可变参数方法。你应该确保方法的主体部分不会抛出类型转换异常ClassCastException或由于对可变参数操作不当而导致的其他类似的异常,你可以通过下列选项之一来忽略编译器产生的警告信息:

  • 在静态的非构造方法的声明上添加注解@SafeVarargs和注解@SuppressWarnings不相同的是,注解@SafeVarargs是方法约定的一部分,这个注解断言在方法的实现中会正确地处理可变参数形式的形式参数。
  • 在方法的声明上添加注解:@SuppressWarnings({"unchecked", "varargs"}) 和注解@SafeVarargs不同的是,该注解不会忽略该方法被调用时产生的警告信息。
  • 使用编译器参数命令:-Xlint:varargs

例如,在下面版本的ArrayBuilder类中有两个额外的方法addToList2和addToList3:

public class ArrayBuilder {

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

  @SuppressWarnings({"unchecked", "varargs"})
  public static <T> void addToList2 (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }

  @SafeVarargs
  public static <T> void addToList3 (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }// ...}public class HeapPollutionExample {// ...public static void main(String[] args) {// ...ArrayBuilder.addToList(listOfStringLists, stringListA, stringListB);
    ArrayBuilder.addToList2(listOfStringLists, stringListA, stringListB);
    ArrayBuilder.addToList3(listOfStringLists, stringListA, stringListB);// ...}
}

Java编译器将会为这个示例产生如下警告信息:

  • addToList:
    • 在方法定义处: [unchecked] Possible heap pollution from parameterized vararg type T
    • 当方法被调用时: [unchecked] unchecked generic array creation for varargs parameter of type List<String>[]
  • addToList2:当方法被调用时 (在方法定义时不会产生警告): [unchecked] unchecked generic array creation for varargs parameter of type List<String>[]
  • addToList3: 在方法定义和方法调用时都不会产生任何警告信息。

注意: 在Java SE 5和Java SE 6中,调用非具体化形式参数的可变参数方法的程序员有责任去确定哪些情况可能导致堆污染。 然而,如果程序员没有编写过类似这样的方法,那么他/她也很难确定。在Java SE 7中,编写这些可变参数方法的程序员有责任确保他们能够正确处理可变参数形式的形式参数,并且不会导致堆污染的发生。

  • CodePlayer技术交流群1
  • CodePlayer技术交流群2

0 条评论

撰写评论

打开导航菜单