可能发生堆污染通过可变参数参数

539

我了解当在使用泛型类型和可变参数(varargs)时,Java 7会出现这种情况;

但我的问题是..

当Eclipse说“它的使用可能会污染堆(heap)”时,它到底是什么意思?

还有,

新的@SafeVarargs注解如何防止这种情况发生?


11
详情请参见:http://docs.oracle.com/javase/specs/jls/se7/html/jls-9.html#jls-9.6.3.7 - assylias
我在我的编辑器中看到了这个:可能会出现参数化可变参数类型的堆污染 - Alexander Mills
2
如果你对于何时可以安全地使用注解@SafeVarargs感到困惑(就像我一样),这里有一个有用的解释,可以检查你的方法是否适合使用@SafeVarargs进行注解。 - Qw3ry
我想知道为什么Java会有这个问题而C#却没有。Java真的那么糟糕吗? - bikeman868
6个回答

314
堆污染是一个技术术语。它指的是引用具有与其所指向的对象不是超类型的类型。
List<A> listOfAs = new ArrayList<>();
List<B> listOfBs = (List<B>)(Object)listOfAs; // points to a list of As

这可能导致“无法解释”的ClassCastException。
// if the heap never gets polluted, this should never throw a CCE
B b = listOfBs.get(0); 

@SafeVarargs并不能完全防止这种情况发生。然而,有一些方法可以证明不会污染堆,只是编译器无法证明。以前,调用这些API的调用者会收到完全无意义但必须在每个调用点抑制的烦人警告。现在,API的作者可以在声明处一次性抑制它。
然而,如果方法实际上是不安全的,用户将不再收到警告。

2
那么我们是不是在说堆被污染了,因为它包含了类型不符合我们预期的引用?(例如你的例子中的List<A> vs List<B>) - hertzsprung
47
我在程序员交流网站上找到了一个有关"堆污染"的解释。 - hertzsprung
52
这个回答对于什么是堆污染给出了很好的解释,但它并没有真正解释为什么可变参数特别容易导致堆污染,以至于需要一个特定的警告。 - Dolda2000
8
我也有同样的问题,不知道如何确保我的代码不包含这个问题(例如,如何确定它已经足够强大,可以添加@SafeVarargs)。 - Daniel Alder

298

当你声明

public static <T> void foo(List<T>... bar)时,编译器会将其转换为

public static <T> void foo(List<T>[] bar),然后再转换为

public static void foo(List[] bar)

这时就会出现错误,因为你可能会错误地将不正确的值分配到列表中,而编译器不会触发任何错误。例如,如果T是一个String,那么以下代码将在编译时没有错误,但在运行时会失败:

// First, strip away the array type (arrays allow this kind of upcasting)
Object[] objectArray = bar;

// Next, insert an element with an incorrect type into the array
objectArray[0] = Arrays.asList(new Integer(42));

// Finally, try accessing the original array. A runtime error will occur
// (ClassCastException due to a casting from Integer to String)
T firstElement = bar[0].get(0);

如果您已经检查了方法,确保它不包含此类漏洞,则可以使用@SafeVarargs注解来抑制警告。对于接口,请使用@SuppressWarnings("unchecked")
如果您收到以下错误消息:
可变参数方法可能会导致非重写变量参数的堆污染
并且您确定您的用法是安全的,则应改用@SuppressWarnings("varargs")。有关这种第二种类型错误的很好解释,请参见Is @SafeVarargs an appropriate annotation for this method?https://dev59.com/QGYq5IYBdhLWcg3wvzKm#14252221 参考文献:

3
我觉得我理解得更好了。危险在于将可变参数转换为 Object[]。只要不转换为 Object[],听起来应该没问题。 - djeikyb
3
作为一个愚蠢的例子,你可以这样做:static <T> void bar(T...args) { ((Object[])args)[0] = "a"; }。然后调用 bar(Arrays.asList(1,2)); - djeikyb
1
@djeikyb 如果只有在我转换为 Object[] 时才会出现危险,那么如果我不转换,为什么编译器会触发警告呢?毕竟,在编译时检查这一点应该相当容易(除非我将其传递给具有类似签名的另一个函数,在这种情况下,另一个函数应该触发警告)。 我不认为这真的是警告的核心(“如果你不转换,你就安全了”),而且我仍然不明白在哪种情况下我是安全的。 - Qw3ry
7
@djeikyb 你可以不使用参数化的可变参数(例如 bar(Integer...args))而做完全相同的愚蠢事情。那么这个警告的意义是什么呢? - Vasiliy Vlasov
5
这个问题只涉及参数化的可变参数。如果你试图使用非类型化数组做同样的事情,运行时会阻止你将错误的类型插入到数组中。编译器警告你说,在运行时无法防止不正确的行为,因为参数类型在运行时是未知的(相比之下,数组在运行时知道它们的非泛型元素的类型)。 - Gili
显示剩余3条评论

11

@SafeVarargs并不会防止堆污染的发生,而是规定编译器在编译使用它的代码时更加严格。

详细信息请参考http://docs.oracle.com/javase/7/docs/api/java/lang/SafeVarargs.html

堆污染指的是,在对泛型接口执行操作时,如果它包含一个声明类型不同的其他类型,则会出现ClassCastException


其额外的编译器限制似乎并不特别相关。 - Paul Bellora

7
当你使用可变参数时,可能会创建一个 Object[] 来保存这些参数。
由于逃逸分析,JIT 可以优化掉这个数组的创建。(这是我发现它这样做的少数情况之一) 不能保证它被优化掉,但如果在您的内存分析器中看到它是一个问题,那么才需要担心。
据我所知,@SafeVarargs 抑制了编译器的警告,并不改变 JIT 的行为。

6
虽然很有趣,但它并没有真正回答他关于@SafeVarargs的问题。 - Paul Bellora
2
不对。那不是堆污染(Heap Pollution)的定义。“堆污染发生在参数化类型的变量引用了一个不属于该参数化类型的对象时。” 参考:http://docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html#heap_pollution - Doradus

2

原因是可变参数允许使用非参数化对象数组进行调用。因此,如果您的类型是List <A> ...,它也可以使用List[]非可变参数类型进行调用。

以下是一个示例:

public static void testCode(){
    List[] b = new List[1];
    test(b);
}

@SafeVarargs
public static void test(List<A>... a){
}

正如您所看到的,List[] b 可以包含任何类型的 consumer,但是这段代码可以编译通过。如果您使用 varargs,则可以正常使用,但是如果您在 type-erasure 后使用方法定义 - void test(List[]) - 那么编译器将不会检查模板参数类型。@SafeVarargs 将抑制此警告。


1
当您可以控制方法的调用方式时(例如,类的私有方法),在方法中添加@SafeVarargs注释相对安全。您必须确保只有声明的泛型类型的实例传递给该方法。
如果将该方法作为库外部公开,则很难捕获此类错误。在这种情况下,最好避免使用此注释,并使用集合类型(例如Collection<Type1<Type2>>)输入而不是可变参数(Type1<Type2>...)重写解决方案。
至于命名,我认为术语“堆污染”现象非常误导人。在文档中,实际的JVM甚至没有被提及。在软件工程中有一个问题,其中包含有关此现象命名的一些有趣想法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接