两个 sn-ps 的破坏方式不同!
案例 1(Collection<String> col):
由于Collection 没有索引,其接口公开的唯一remove 方法是Collection.remove(Object o),它会删除指定的相等对象。执行col.remove(1); 首先调用Integer.valueOf(1) 以获取Integer 对象,然后要求列表删除该对象。由于该列表不包含任何此类Integer 对象,因此不会删除任何内容。在列表中正常进行迭代并打印出abc。
案例 2(ArrayList<String> col):
当col的编译时类型为ArrayList时,调用col.remove(1);改为调用ArrayList.remove(int index)方法移除指定位置的元素,从而移除b。
现在,为什么没有打印出c?为了使用for (X : Y) 语法遍历集合,它在后台调用集合以获取Iterator 对象。对于ArrayList(以及大多数集合)返回的Iterator在迭代期间对列表进行结构修改是不安全的——除非您通过Iterator 本身的方法对其进行修改– 因为Iterator 会变得混乱并且不知道接下来要返回哪个元素。这可能导致元素被多次迭代、元素被跳过或其他错误。这就是这里发生的情况:元素 c 存在于列表中,但从未打印出来,因为您混淆了 Iterator。
当Iterator 可以检测到此问题已发生时,它会通过抛出ConcurrentModificationException 来警告您。但是,Iterator 针对问题所做的检查针对速度进行了优化,而不是 100% 正确,并且它并不总是能检测到问题。在您的代码中,如果您将s.equals("b") 更改为s.equals("a") 或s.equals("c"),它会抛出异常(尽管这可能取决于特定的Java 版本)。来自ArrayList documentation:
这个类的iterator 和listIterator 方法返回的迭代器是fail-fast:如果列表在创建迭代器后的任何时候在结构上被修改,除了通过迭代器自己的remove 或add 方法,迭代器会抛出一个ConcurrentModificationException。因此,面对并发修改,迭代器快速而干净地失败,而不是在未来不确定的时间冒任意的、非确定性的行为。
请注意,无法保证迭代器的快速失败行为,因为一般来说,在存在不同步的并发修改的情况下无法做出任何硬保证。快速失败的迭代器会尽最大努力抛出ConcurrentModificationException。
要在迭代期间删除元素,您必须将循环的for (X : Y) 样式更改为显式Iterator 上的手动循环,使用其remove 方法:
for (Iterator<String> it = col.iterator(); it.hasNext();) {
String s = it.next();
if (s.equals("b"))
it.remove();
System.out.print(s);
}
现在这是完全安全的。它将所有元素只迭代一次(打印abc),而元素b 将被删除。
如果您愿意,您可以在不使用Iterator 的情况下使用int i 样式的循环来实现相同的效果,前提是您在删除后仔细调整索引:
for (int i = 0; i < col.size(); i++) {
String s = col.get(i);
if (s.equals("b")) {
col.remove(i);
i--;
}
System.out.print(s);
}