我正在阅读关于变量堆污染的文章,我真的不明白变量或非可实现类型如何导致没有泛型就不存在的问题。实际上,我可以很容易地替换
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
的子类,我仍然可以将任何对象分配给数组。)当然,我可以看出两者之间有一点区别,但无论是否使用泛型,这段代码都是错误的。
他们所说的堆污染是什么意思(这让我想到内存使用,但他们谈论的唯一问题是潜在的类型不安全),它与使用数组协方差的任何类型冲突有何不同?
你是对的,常见的(和基本的)问题是与数组的协方差。但在你给出的两个例子中,第一个更危险,因为它可以修改你的数据结构,并使它们处于一种以后会崩溃的状态。
考虑你的第一个例子是否没有触发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更重要。
数组和List的区别在于数组检查它的引用。例如
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所说的"堆污染"是指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的类型擦除系统是无操作的。
Integer
插入到List<String>
中。然后,当我们试图对一个元素进行get()
操作时,列表对象没有遵守它必须返回String
(或null
)的承诺。
在泛型出现之前,对象的运行时类型绝对不可能与其静态类型不一致。这显然是一个非常理想的特性。
可以将对象强制转换为不正确的运行时类型,但强制转换会立即在强制转换的确切位置失败;此错误停止。
Object obj = "string";
((Integer)obj).intValue();
// we are not gonna get an Integer object
随着泛型和类型擦除(所有弊端的根源)的引入,现在有可能一个方法在编译时返回String
,而在运行时返回Integer
。这是一团糟。我们应该尽我们所能从源头上阻止它。这就是为什么编译器对每次未检查的强制转换都如此直言不讳。
堆污染最糟糕的事情是运行时行为未定义!不同的编译器/运行时可能以不同的方式执行程序。参见案例1和案例2
它们不同,因为ClassCastException
和ArrayStoreException
不同。
泛型编译时类型检查规则应该确保在没有显式强制转换的地方不可能得到ClassCastException
,除非你的代码(或你调用或调用你的代码)在编译时做了一些不安全的事情,在这种情况下,你应该(或任何代码做了不安全的事情)收到一个编译时警告。
ArrayStoreException
是Java中数组工作的正常部分,并且早于泛型。由于Java中数组类型系统的设计方式,编译时类型检查不可能阻止ArrayStoreException
。