【问题标题】:varargs heap pollution : what's the big deal?可变参数堆污染:有什么大不了的?
【发布时间】:2018-07-24 05:21:26
【问题描述】:

我正在阅读有关 varargs heap pollution 的信息,但我并没有真正理解可变参数或不可具体化类型将如何对没有通用性的情况下不存在的问题负责。确实,我可以很容易地替换

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&lt;String&gt; 是可具体化的,我想它仍然是Object 的子类,我仍然可以将任何对象分配给数组。)当然我可以看到两者之间有一点区别,但是无论是否使用泛型,这段代码都是错误的。

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

【问题讨论】:

  • 好问题,请允许我添加行 objectArray[0] = 42;是实际抛出 ArrayStoreException 的那个。

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


【解决方案1】:

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

考虑您的第一个示例是否触发了 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;

所以现在我们有了一个实际上包含一个IntegerList&lt;String&gt;,并且它安全地漂浮在周围。在稍后的某个时刻——可能更晚,如果它被序列化,可能更晚并且在不同的 JVM 中——终于有人执行String s = theList.get(0)。此故障与导致它的原因相距甚远,因此可能很难追查。

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

【讨论】:

  • 是的,这就是我在阅读其他两个答案时得出的结论,这些答案似乎不像您的答案那样明确,但帮助我理解了它。 Humpf,我将很难选择获胜者。
  • @yshavit,我研究了你的答案,发现它很有趣,谢谢分享...如果你使用faultyMethod(List&lt;String&gt;.... l)的方法,我想补充一下;将元素作为 faultyMethod(firstList, secondList) 之类的单个元素或faultyMethod(new List[] {firstList, secondList} ) 之类的匿名数组传递,您希望在方法之外有任何问题,因为被修改的对象是列表数组,无论是显式还是隐式(varg args 为您执行此操作)用作方法唯一参数。
  • 我的意思是,如果你做这样的事情,你会遇到函数范围之外的问题:List&lt;String&gt;[] array = new List[] {firstList, secondList}; faultyMethod(array); System.out.println(array[0] + " " + array[1]);
  • @Dici 以我对事物的理解,我的建议是尽量不要混合数组和泛型类型。 Vargs args 甚至给代码增加了更多的混乱。当未经检查的警告出现时......这意味着你应该尝试改变事情。这将是最后的建议,我前段时间在 Joshua 的《Effective Java》一书中读到了它......直到现在我还没有找到更好的建议。
【解决方案2】:

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

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.

【讨论】:

  • 是的,我知道这一点。我猜你的意思是数组被认为更安全,因为我无法在运行时欺骗他(ArrayStoreException),而List 不会抱怨,因此它们会在编译时生成警告?
  • @Dici 是的,这就是我的意思。 +1
【解决方案3】:

从链接的文档中,我相信 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&lt;String&gt;。然后,当我们尝试get() 一个元素时,列表对象未能履行其必须返回String(或null)的承诺。

【讨论】:

  • 是的,我知道这一点,但我想通过你的回答和 Peter Lawrey 的回答,我可以明白他们为什么这样做。数组将在插入时立即抛出异常,而通用列表(例如)只会在读取值时失败,这可能不会发生或在插入后很长时间才会发生,从而使调试更加困难。是你的意思吗?
【解决方案4】:

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

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

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

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

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

【讨论】:

  • 谢谢。现在我对此有了更清晰的认识,我怀疑桥接方法是造成奇怪的 ClassCastException 的原因,我有时会在使用 Spark 序列化闭包时遇到这种情况
【解决方案5】:

它们不同是因为ClassCastExceptionArrayStoreException 不同。

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

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

【讨论】:

    猜你喜欢
    • 2015-05-08
    • 2016-12-11
    • 2012-09-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-11-21
    相关资源
    最近更新 更多