【Java7】改进使用带泛型可变参数的方法时的编译器警告和错误提示

堆污染

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

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

先看下面一段代码:

 
 
 
 
 
  1. List l = new ArrayList<Number>();

  2. List<String> ls = l;       // unchecked warning

  3. l.add(0, new Integer(42)); // another unchecked warning

  4. 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忽略了该警告)[email protected]�参数的方法时,相对于在方法调用时(产生警告)而言,在方法声明时产生警告的好处在于方法的声明只有一个,而方法调用的地方可能有多个。

方法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是方法约定的一部分,[email protected]�数形式的形式参数。

  • 在方法的声明上添加注解:@SuppressWarnings({"unchecked", "varargs"}) 和注解@SafeVarargs不同的是,[email protected]

  • 使用编译器参数命令:-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中,编写这些可变参数方法的程序员有责任确保他们能够正确处理可变参数形式的形式参数,并且不会导致堆污染的发生。

原文出处:http://www.365mini.com/page/13.htm

赞(52) 打赏
未经允许不得转载:优客志 » JAVA开发
分享到:

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏