iterator.remove()
和 list.remove()
有什么不同之处,以至于迭代器不会抛出异常而 list.remove()
会抛出异常?最终,两者都会修改集合大小。
请忽略多线程。我只是在谈论一个 for-each 循环和一个迭代器循环。据我所知,for-each 循环仅在内部创建迭代器。
我感到困惑。
iterator.remove()
和 list.remove()
有什么不同之处,以至于迭代器不会抛出异常而 list.remove()
会抛出异常?最终,两者都会修改集合大小。
请忽略多线程。我只是在谈论一个 for-each 循环和一个迭代器循环。据我所知,for-each 循环仅在内部创建迭代器。
我感到困惑。
我想你的意思是,如果你正在迭代一个列表,为什么使用list.remove()
会导致抛出ConcurrentModificationException
而iterator.remove()
则不会呢?
考虑以下示例:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (Iterator<String> iter = list.iterator(); iter.hasNext(); ) {
if (iter.next().equals("b")) {
// iter.remove(); // #1
// list.remove("b"); // #2
}
}
如果你取消注释第一行,它将正常工作。如果你取消注释第二行(但注释掉第一行),那么后续对iter.next()
的调用将导致抛出ConcurrentModificationException
异常。
原因是迭代器是一个单独的对象,它具有对基础列表内部状态的一些引用。如果你在迭代器正在操作时修改了列表,可能会导致迭代器表现不良,例如跳过元素、重复元素、超出数组末尾索引等。它试图检测这种修改,如果发现就会抛出ConcurrentModificationException
异常。
通过迭代器删除元素可以正常工作且不会导致异常,因为这会更新基础列表和引用内部列表的迭代器状态,从而使所有内容保持一致。
然而,iterator.remove()
并没有特别之处,可以让它在所有情况下正常工作。如果有多个迭代器遍历同一个列表,由一个迭代器所做的修改将会给其他迭代器带来问题。考虑以下情况:
Iterator<String> i1 = list.iterator();
Iterator<String> i2 = list.iterator();
i1.remove();
i2.remove();
我们现在有两个指向同一列表的迭代器。如果我们使用其中一个来修改列表,则会干扰第二个的操作,因此调用 i2.remove()
将导致 ConcurrentModificationException
异常。
ConcurrentModificationException
不会被 Iterator.remove()
抛出,因为这是迭代过程中修改集合的允许方式。这是 Iterator
的javadoc 中所述的:
从基础集合中删除此迭代器最后一个返回的元素(可选操作)。每次调用 next() 方法只能调用一次此方法。如果通过除此方法以外的任何方式修改正在进行迭代的基础集合,则迭代器的行为是未指定的。
如果您以其他方式更改正在迭代的集合,则可能会根据迭代器的实现和您正在迭代的集合(或其他内容)而得到异常。(某些集合类不会给您抛出 ConcurrentModificationException
:请查看各自的javadoc,了解它们迭代器的行为如何指定)
如果您在同一集合上有两个迭代器,并且通过其中一个删除,则也可能会得到异常。
与 list.remove 相比,iterator.remove 有什么不同之处,iterator 不会抛出异常而 list.remove 会抛出?
原因 #1. 如果你同时从同一调用堆栈的两个位置更新非并发集合,那么行为将会破坏迭代的设计不变式1。 对非并发集合的迭代保证恰好能够看到集合中所有元素各一次。(相比之下,并发集合的这些保证是放松的)。
原因 #2. 非并发集合类型没有实现为线程安全。 因此,如果使用集合和迭代器来由不同的线程更新集合,则可能存在竞争条件和内存异常。 这不是一个强有力的原因,因为您无论如何都会遇到这些问题。 但是,以两种不同的方式进行更新会使问题变得更糟。
我只是在谈论 for-each 循环和 iterator 循环。 据我所知,for-each 循环在内部仅创建迭代器。
没错,for-each 循环只是使用迭代器的 while
循环的语法糖。
另一方面,如果您使用以下循环:
for (int i = 0; i < list.size(); i++) {
if (...) {
list.remove(i);
}
}
你不会遇到ConcurrentModificationException
,但你需要调整删除元素的索引变量,并且其他线程的更新可能会导致你跳过某些元素或多次访问它们2。
1 - 为了实现“恰好一次”迭代行为,当您通过集合对象删除元素时,迭代器数据结构需要更新以使其与已发生的更改保持同步。由于当前的实现不保留与未完成的迭代器的链接,因此这是不可能的。而如果它们这样做,它们将需要使用Reference
对象或面临内存泄漏的风险。
2 - 或甚至获得IndexOutOfBoundsException
。如果集合不是并发/正确同步的,你可能会遇到更严重的问题。
因为是迭代器抛出异常。如果调用 List.remove()
,列表不知道元素已被移除,只知道某些东西在它的“脚下”发生了变化。如果调用 Iterator.remove()
,则知道当前元素已被移除及如何处理。
public class ArrayListExceptionTest {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
list1.add("a");
list1.add("b");
list1.add("c");
Iterator<String> it1 = list1.iterator();
ArrayList<String> list2 = new ArrayList<String>();
list2.add("a");
try {
while (it1.hasNext()) {
list1.add(it1.next());
}
} catch (ConcurrentModificationException e) {
e.printStackTrace();
}
it1 = list1.iterator();
while (it1.hasNext()) {
System.out.println(it1.next());
}
it1 = list1.iterator();
try {
while (it1.hasNext()) {
if (it1.next().equals("a"))
list1.retainAll(list2);
}
} catch (ConcurrentModificationException e) {
e.printStackTrace();
}
it1 = list1.iterator();
while (it1.hasNext()) {
System.out.println(it1.next());
}
it1 = list1.iterator();
Iterator<String> it2 = list1.iterator();
it1.remove();
it2.remove();
}
}
您可以看到上述3种情况:
情况1:通过添加元素进行修改,因此在使用next()函数时会导致ConcurrentModificationException异常。
情况2:通过使用retain()进行修改,因此在使用next()函数时会导致ConcurrentModificationException异常。
情况3:将抛出java.lang.IllegalStateException而不是ConcurrentModificationException异常。
输出:
a
b
c
a
a
a
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.rms.iteratortest.ArrayListExceptionTest.main(ArrayListExceptionTest.java:21)
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.rms.iteratortest.ArrayListExceptionTest.main(ArrayListExceptionTest.java:37)
Exception in thread "main" java.lang.IllegalStateException
at java.util.ArrayList$Itr.remove(ArrayList.java:872)
at com.rms.iteratortest.ArrayListExceptionTest.main(ArrayListExceptionTest.java:55)
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
at Collection.IteratorDemo.main(IteratorDemo.java:16)
你可以在上面的错误日志中检查第三行。
List<Integer> nums = new ArrayList<>();
nums.add(1);
nums.add(2);
for(int i : nums){
nums.remove(1);
System.out.println(i);
}
protected transient int modCount = 0;
这个变量通过在添加/删除集合调用中增加和减少值来维护集合的结构更改。这就是集合实现快速失败迭代器的方式。
这里举个例子,如果集合迭代器没有检查底层集合的修改情况,事情可能会出错。这就是 ArrayList
迭代器的实现方式:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
public E next() {
checkForComodification();
int i = cursor;
if (i >= size) throw new NoSuchElementException();
// ...
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
// ...
ArrayList.this.remove(lastRet);
// ...
cursor = lastRet;
lastRet = -1;
}
让我们看一个例子:
List list = new ArrayList(Arrays.asList(1, 2, 3, 4));
Iterator it = list.iterator();
Integer item = it.next();
我们移除了第一个元素
list.remove(0);
it.remove()
,迭代器将会移除 number 2,因为这是字段 lastRet
现在所指向的内容。if (item == 1) {
it.remove(); // list contains 3, 4
}
这将是不正确的行为!迭代器的契约规定remove()
删除next()
返回的最后一个元素,但在存在并发修改的情况下,它无法保持其契约。因此,它选择保守起见并抛出异常。
对于其他集合,情况可能更加复杂。如果您修改了HashMap
,它可能会根据需要增长或缩小。此时,元素将落入不同的桶中,并且在重新哈希之前保持指向桶的迭代器将完全丢失。
请注意,iterator.remove()
本身不会抛出异常,因为它能够更新自身和集合的内部状态。但是,在同一实例集合的两个迭代器上调用remove()
将引发异常,因为它会使其中一个迭代器处于不一致状态。
Iterator
类被设计成以这种方式工作...并且规定以这种方式工作。设计/规范导致了实现,而不是相反。 - Stephen C