可变参数堆污染:有什么大不了的?

varargs heap pollution : what's the big deal?

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

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

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

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.

从链接文档中,我相信 Oracle "heap pollution" 的意思是具有 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];

扭曲 - 现在这里的代码使用原始类型并做了一些不好的事情,导致 "heap pollution":

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 的类型擦除系统。

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

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

考虑一下您的第一个示例是否没有触发 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 的堆栈跟踪并没有告诉我们错误真正发生的位置;它只是告诉我们是谁触发了它。换句话说,它并没有给我们太多关于如何修复错误的信息;这就是它比 ArrayStoreException 更重要的原因。

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

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

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

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

堆污染最糟糕的事情是运行时行为是未定义的!不同的compiler/runtime可能会以不同的方式执行程序。参见 case1 and case2

它们不同是因为 ClassCastExceptionArrayStoreException 不同。

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

另一方面,

ArrayStoreException 是数组在 Java 中工作的正常部分,并且早于泛型。由于在 Java.

中设计数组类型系统的方式,编译时类型检查无法阻止 ArrayStoreException