admin管理员组

文章数量:1530845

    当Java 泛型和可变参数配合使用时IDE会给出该警告:Type safety: Potential heap pollution via varargs parameter args,字面意思潜在的堆污染,不太能理解什么意思,百度一下没有找到好的解释,遂查阅Java相关文档和stackoverflow, 再结合自己的思考得出了自己的看法。
首先上一段代码

public class ArrayBuilder {
	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
	}

	public static void main(String[] args) {
		ArrayBuilder.faultyMethod(Arrays.asList("Hello!"),
				Arrays.asList("World!"));
	}
}

在faultyMethod方法处会提示heap pollution警告,并且运行起来后会抛出ClassCastException异常,首先List<String>… 会被编译成List[], 然后 Object[] objectArray = l 是合法的,因为数组是协变的,子类数组赋值给父类数组是合法的,接下来objectArray[0] = Arrays.asList(42) 同样也是合法的,因为泛型擦除后,l 是List[] 并且 Arrays.asList(42)是List, 因此这句在运行期也不会抛出任何异常,一个List<Integer> 成功混进了List<String>[] 进而String s = l[0].get(0) 抛出了类型转换异常。
    但是这和heap pollution有什么关系呢,我们知道泛型是会进行编译期类型检查,如果l[0] = Arrays.asList(42); 编译无法通过,因为l的声明类型为 List<String>[], 自然不会允许 List<Integer> 的元素类型; 数组与泛型不同的是数组对象在运行期间的类型信息也是保留的,

 Integer[] iArray = new Integer[10];
 Object[] oArray = iArray;
 oArray[0] = "a";

这段代码可以编译通过,但是运行期间会抛出ArrayStoreException 异常,在运行期数组会进行类型检查防止错误的类型的元素被加进数组中。但是泛型和数组组合起来事情就复杂了,赋值给Object[] 导致了失去了编译期泛型检查能力, 同时因为泛型擦除,运行期只要是个List类型就可以加入到*List[]*数组中,最终导致了 List<String>[] 中出现了 List<Integer> 对象,原数组被错误类型的数据污染了。
类似情况还有这种

List<String> asList = new ArrayList<String>();
List rawList = asList;
rawList.add(Arrays.asList(1));
String str = asList.get(0);

这种代码也会抛出ClassCastException 异常,在第二句及第三句都会给出 raw type 相关的警告,raw type 容器是为了兼容性,平时我们写代码应该避免。回到最开始的例子中,我们可以看到faultyMethod 方法中的每一行单拿出来看都是合法的,但是组合到一起就出现了错误,无论在哪一行加警告都不合适,因此编译器干脆在faultyMethod 加上了一类特殊的Heap Pollution警告信息,专门用来警告可变参数和泛型混用的情况。如果想消除警告,可以使用@SafeVarargs 注解。

    除了上述例子外,还有另一种Heap Pollution 情况:

public static <T> T[] foo(T... args) {
		return args;
	}

public static <T> T[] bar(T... args) {
	return foo(args[0]);
}

public static void main(String[] args) {
	List<Integer>[] bar = bar(Arrays.asList(2));
}

这里代码会抛出 java.lang.ClassCastException , 为了便于说明直接看这段代码反编译结果:

public static transient Object[] foo(Object args[])
{
    return args;
}

public static transient Object[] bar(Object args[])
{
    return foo(new Object[] {
        args[0]
    });
}

public static void main(String args[])
{
    Integer bar[] = (Integer[])bar(new Integer[] {
        Integer.valueOf(1), Integer.valueOf(2)
    });
}

可以看到调用 bar 时编译期自动为我们创建了一个Integer数组,这是因为编译器具可以根据参数进行类型推断。但是在 bar 方法中调用 foo 时创建的数组却是 Object[], 这是因为在泛型方法内部无法创建 T[],所有的类型参数全部擦除到边界类型,在 foobarT 的边界类型都是 Object, 因此编译后所有的 T 都成了 Object, args[0] 在编译期也是 Object, 因此根据参数推断,编译器自然只能帮我们创建 Object[] 数组。
    那么这种情况下我们怎样才能判断出我们的代码是否安全呢,原则就是如果不依赖数组的运行时类型,就会是安全的。

参考资料:
https://docs.oracle/javase/tutorial/java/generics/nonReifiableVarargsType.html#vulnerabilities
https://stackoverflow/questions/14231037/java-safevarargs-annotation-does-a-standard-or-best-practice-exist/14252221#14252221

本文标签: Javaheappollution