18

我正在阅读有关varargs 堆污染的内容,但我并没有真正了解 varargs 或不可具体化的类型将如何对没有通用性的情况下不存在的问题负责。确实,我可以很容易地替换

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 faultyMethod(String... l) {
    Object[] objectArray = l; // Valid
    objectArray[0] = 42;  // ArrayStoreException thrown here
    String s = l[0];
}

第二个简单地使用了数组的协方差,这确实是这里的问题。(即使List<String>是可具体化的,我想它仍然是的子类,Object我仍然可以将任何对象分配给数组。)当然我可以看到两者之间有一点区别,但是这段代码是否有问题是否使用泛型。

他们所说的堆污染是什么意思(这让我想到了内存使用,但他们谈论的唯一问题是潜在的类型不安全),它与使用数组协方差的任何类型违规有何不同?

4

5 回答 5

14

你是对的,常见的(和基本的)问题是数组的协方差。但是在你给出的这两个例子中,第一个更危险,因为它可以修改你的数据结构并将它们置于稍后会中断的状态。

考虑您的第一个示例是否没有触发 ClassCastException:

public static void faultyMethod(List<String>... l) {
  Object[] objectArray = l;           // Valid
  objectArray[0] = Arrays.asList(42); // Also valid
}

以下是有人使用它的方式:

List<String> firstList = Arrays.asList("hello", "world");
List<String> secondList = Arrays.asList("hello", "dolly");
faultyMethod(firstList, secondList);
return secondList.isEmpty()
  ? firstList
  : secondList;

所以现在我们有一个List<String>实际上包含一个 的a Integer,并且它安全地漂浮在周围。在稍后的某个时刻——可能更晚,如果它被序列化,可能晚并且在不同的 JVM 中——终于有人执行String s = theList.get(0). 这次失败与导致它的原因相去甚远,以至于很难追查到。

请注意,ClassCastException 的堆栈跟踪并没有告诉我们错误真正发生的位置。它只是告诉我们是谁触发了它。换句话说,它并没有为我们提供太多关于如何修复错误的信息。这就是它比 ArrayStoreException 更重要的原因。

于 2015-08-29T23:30:42.620 回答
8

数组和列表的区别在于数组检查它的引用。例如

Object[] array = new String[1];
array[0] = new Integer(1); // fails at runtime.

然而

List list = new ArrayList<String>();
list.add(new Integer(1)); // doesn't fail.
于 2015-08-29T23:10:29.857 回答
5

从链接的文档中,我相信 Oracle 所说的“堆污染”是指具有 JVM 规范在技术上允许但 Java 编程语言中的泛型规则不允许的数据值。

举个例子,假设我们定义了一个List像这样的简单容器:

class List<E> {
    Object[] values;
    int len = 0;

    List() { values = new Object[10]; }

    void add(E obj) { values[len++] = obj; }
    E get(int i) { return (E)values[i]; }
}

这是一个通用且安全的代码示例:

List<String> lst = new List<String>();
lst.add("abc");

这是一个使用原始类型(绕过泛型)但仍然在语义级别上尊重类型安全的代码示例,因为我们添加的值具有兼容的类型:

String x = (String)lst.values[0];

扭曲 - 现在这里的代码适用于原始类型并且做一些不好的事情,导致“堆污染”:

lst.values[lst.len++] = new Integer("3");

上面的代码有效,因为数组的类型是Object[],它可以存储一个Integer. 现在,当我们尝试检索该值时,它会ClassCastException在检索时间(发生损坏之后)导致 - 而不是在添加时间:

String y = lst.get(1);  // ClassCastException for Integer(3) -> String

请注意,这ClassCastException发生在我们当前的堆栈帧中,甚至没有发生在 中List.get(),因为List.get()由于 Java 的类型擦除系统,强制转换在运行时是无操作的。

基本上,我们通过绕过泛型将 an 插入Integer到 a中。List<String>然后,当我们尝试get()一个元素时,列表对象未能遵守它必须返回一个String(或null)的承诺。

于 2015-08-29T23:14:06.420 回答
3

在泛型之前,绝对不可能对象的运行时类型与其静态类型不一致。这显然是一个非常理想的属性。

我们可以将对象转换为不正确的运行时类型,但转换会立即在转换的确切位置失败;错误停在那里。

Object obj = "string";
((Integer)obj).intValue();
// we are not gonna get an Integer object

随着泛型的引入,以及类型擦除(万恶之源),现在方法可能String在编译时返回,但Integer在运行时返回。这是一团糟。我们应该尽我们所能从源头上阻止它。这就是为什么编译器对每一次未经检查的强制转换都如此直言不讳。

堆污染最糟糕的事情是运行时行为未定义!不同的编译器/运行时可能以不同的方式执行程序。请参见case1case2

于 2015-08-30T03:40:35.200 回答
1

它们是不同的,因为ClassCastException并且ArrayStoreException是不同的。

泛型编译时类型检查规则应确保不可能ClassCastException在您没有进行显式强制转换的地方获得 a ,除非您的代码(或您调用或调用的某些代码)在编译时做了一些不安全的事情,在在这种情况下,您应该(或任何不安全的代码应该)收到关于它的编译时警告。

ArrayStoreException另一方面,它是数组在 Java 中如何工作的正常部分,并且早于泛型。ArrayStoreException由于数组的类型系统是在 Java 中设计的,因此编译时类型检查无法阻止。

于 2015-09-01T00:21:35.100 回答