varargs堆污染:有什么大不了的?

时间:2015-08-29 22:59:17

标签: java generics memory variadic-functions heap-pollution

我正在阅读关于varargs heap pollution的内容,我并不知道如果没有通用性,那些varargs或non-reifiable类型将对那些尚未存在的问题负责。的确,我可以很容易地取代

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的子类,我仍然可以将任何对象分配给数组。)当然,我可以看到它们之间有一点点差异。这两个,但是这个代码是否有缺陷,无论它是否使用泛型。

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

5 个答案:

答案 0 :(得分:13)

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

考虑一下你的第一个例子是否没有触发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>实际上包含Integer,它安全地漂浮在周围。在某些时候 - 可能更晚,如果它被序列化,可能很多以后和在不同的JVM中 - 有人最终会执行String s = theList.get(0)。这种失败与导致它失败的原因相距甚远。

请注意,ClassCastException的堆栈跟踪并没有告诉我们错误发生在哪里;它只是告诉我们是谁触发了它。换句话说,它没有给我们提供有关如何修复bug的大量信息;这就是使它成为一个比ArrayStoreException更大的交易。

答案 1 :(得分:8)

数组和List之间的区别在于数组检查它的引用。 e.g。

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.

答案 2 :(得分: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()中也是如此,因为由于Java的类型擦除系统,List.get()中的强制转换在运行时是无操作的

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

答案 3 :(得分:3)

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

我们可以将对象强制转换为不正确的运行时类型,但是在强制转换的站点上,强制转换会立即失败;错误在那里停止。

$this->dequeue()

随着泛型的引入,以及类型擦除(所有邪恶的根源),现在有可能一个方法在编译时返回Object obj = "string"; ((Integer)obj).intValue(); // we are not gonna get an Integer object ,但在运行时返回String。这搞砸了。我们应该尽一切努力阻止来源。这就是为什么编译器对未经检查的演员阵容的每一个视觉都如此直言不讳。

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

答案 4 :(得分:1)

它们不同,因为ClassCastExceptionArrayStoreException不同。

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

另一方面,

ArrayStoreException是数组如何在Java中工作的正常部分,并且是Generics的早期版本。编译时类型检查不可能阻止ArrayStoreException,因为数组的类型系统是用Java设计的。