【发布时间】:2014-09-11 11:15:44
【问题描述】:
iterator.remove() 与list.remove() 有何不同,因此迭代器不会抛出异常而list.remove() 会抛出异常?最后,两者都在修改集合大小。
请忽略这里的多线程。我只是在谈论一个 for-each 循环和一个迭代器循环。据我所知,for-each 循环仅在内部创建一个迭代器。
我很困惑。
【问题讨论】:
标签: java collections foreach iterator
iterator.remove() 与list.remove() 有何不同,因此迭代器不会抛出异常而list.remove() 会抛出异常?最后,两者都在修改集合大小。
请忽略这里的多线程。我只是在谈论一个 for-each 循环和一个迭代器循环。据我所知,for-each 循环仅在内部创建一个迭代器。
我很困惑。
【问题讨论】:
标签: java collections foreach iterator
我想你的意思是,如果你正在迭代一个列表,为什么 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
}
}
如果您取消注释第 1 行,它将正常工作。如果您取消注释第 2 行(但保留第 1 行注释),则会导致随后对 iter.next() 的调用抛出 ConcurrentModificationException。
原因是迭代器是一个单独的对象,它对底层列表的内部状态有一些引用。如果在迭代器运行时修改列表,可能会导致迭代器表现不佳,例如通过跳过元素、重复元素、索引数组末尾等。它会尝试检测此类修改,因此如果检测到,它会抛出 ConcurrentModificationException。
通过迭代器移除元素有效且不会导致异常,因为这会更新底层列表和引用列表内部的迭代器状态,因此一切都可以保持一致。
但是,iterator.remove() 并没有什么特别之处,可以让它在所有情况下都能正常工作。如果有多个迭代器在同一个列表上进行迭代,那么其中一个进行的修改会给其他人带来问题。考虑:
Iterator<String> i1 = list.iterator();
Iterator<String> i2 = list.iterator();
i1.remove();
i2.remove();
我们现在有两个迭代器指向同一个列表。如果我们使用其中一个来修改列表,它会破坏第二个的操作,因此对i2.remove() 的调用将导致ConcurrentModificationException。
【讨论】:
Iterator 类设计 以这种方式工作......并且指定 以这种方式工作。设计/规范促成了实施,而不是相反。
ConcurrentModificationException 不会被 Iterator.remove() 抛出,因为这是在迭代时修改集合的允许方式。这就是javadoc for Iterator 所说的:
从底层集合中移除此迭代器返回的最后一个元素(可选操作)。每次调用 next() 时只能调用一次此方法。 如果在迭代过程中通过调用此方法以外的任何方式修改了底层集合,则迭代器的行为是未指定的。
如果您以任何其他方式更改正在迭代的集合,那么您可能会遇到异常,具体取决于迭代器的实现以及您正在迭代的集合(或其他)。 (一些集合类不会给你ConcurrentModificationException:检查各自的 javadocs 以了解它们如何指定 它们的迭代器的行为)
如果您在同一个集合上有两个迭代器,并且您通过其中一个删除,您也可能会遇到异常。
iterator.remove 与 list.remove 有何不同之处在于迭代器不会抛出异常而 list.remove 会抛出异常?
原因 #1。如果您从同一调用堆栈的两个位置同时更新一个非并发集合,则该行为将破坏迭代的设计不变量1。保证非并发集合的迭代只能看到集合中的所有元素一次。 (相比之下,在并发集合中,这些保证被放宽了。)
原因 #2。非并发集合类型未实现为线程安全的。因此,如果集合和迭代器用于由不同线程更新集合,您可能会遇到竞争条件和内存异常。这不是 strong 原因,因为无论如何您都会遇到这些问题。但是,以两种不同的方式进行更新会使问题变得更糟。
我只是在谈论 for-each 循环和迭代器循环。据我所知,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(),它会知道当前元素已被删除以及如何处理它。
【讨论】:
下面是一个例子,如果集合迭代器不检查底层集合的修改,事情会如何出错。 ArrayLists的迭代器是这样实现的:
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() 会抛出异常,因为它会使其中一个迭代器处于不一致的状态。
【讨论】:
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)
【讨论】:
用一些额外的低级细节回答这个问题:
在迭代期间下一次调用 next() 方法时抛出 ConcurrentModificationException。
所以它不是抛出这个异常的集合的 remove() 方法,而是它的迭代器实现的 next() 方法。
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)
您可以查看上述错误日志中的第 3 行。
List<Integer> nums = new ArrayList<>();
nums.add(1);
nums.add(2);
for(int i : nums){
nums.remove(1);
System.out.println(i);
}
这个 next() 方法如何知道集合是否被修改?
通过检查变量,AbstractList
protected transient int modCount = 0;
此变量通过在对集合的添加/删除调用中递增和递减值来维护集合的结构更改。 这就是集合实现快速失败迭代器的方式。
【讨论】: