我确实知道,在创建迭代器后,只要对基础集合进行结构化修改,就会抛出ConcurrentModificationExcpetion。这似乎是设计决定。
我想知道为什么CME不考虑通过set()方法进行更新?最终,集合将得到更新,并且如果创建了迭代器,遍历仍可能导致结果不一致。
答案 0 :(得分:2)
除非您确实参与了设计决策,否则不可能知道设计决策的确切原因。
我推测原因可能是迭代器设计为迭代 collection ,而不是迭代collection中的 values。
换句话说,迭代器就像一个“指针”,它将被移动以依次指向集合中的每个“空间”。通过了解构造迭代器时有关集合“形状”的一些知识,您知道如何移动指针以指向集合中的下一个空间。
如果存储在一个空格中的值发生了变化,那并不重要:该空格和集合中的所有其他空格都保持不变。
但是,如果通过结构修改来更改集合的形状,则基本上使关于该指针指向的任何假设无效;除了做一些非常聪明的事情之外,您最好放弃并抛出异常。
答案 1 :(得分:2)
最终,集合将得到更新,并且如果创建了迭代器,遍历仍然可能导致结果不一致。
实际上,我认为原因是set
操作不会导致结果不一致。迭代集合的代码将以高度可预测的方式看到set
调用的修改结果,或者没有看到。并且保证该代码仍然只能看到该集合的每个其他元素(即,除了您“设置”的元素之外)仅一次。
相反,对典型的非并行集合的结构修改可能导致集合条目/元素四处移动。例如:
在ArrayList
中插入或删除会导致后备数组中的元素改变位置。
在HashMap
或HashSet
中插入会导致调整大小,从而导致哈希链被重建。
在两种情况下,结构上的修改都可能导致元素被跳过或多次出现。
答案 2 :(得分:1)
请在下面找到我对您的假设的评论。
我确实知道,在创建迭代器后,只要对基础集合进行结构性修改,就会抛出
ConcurrentModificationException
。
不完全是。实际上,当迭代器尝试获取下一个值并且基础集合中发生结构性更改时,实际上会引发ConcurrentModificationException
。 (我想强调一点,即在基础集合上调用ConcurrentModificationException
或add
时不会引发remove
。
然后,关于您的最后一个假设:
最终,集合将得到更新,并且如果创建了迭代器,遍历仍然可能导致结果不一致。
我认为这是不正确的。遍历如何仍会导致结果不一致?如果您修改列表第i个位置的值,则会立即看到此更改。该列表实际上已修改,但没有结构上修改。仅第i个位置的值发生更改,列表结构(即ArrayList
的基础数组不会改变)的大小不会改变,LinkedList
不会分配新的大小节点等)。
最后,关于这句话:
这似乎是设计决定。
绝对。这是100%的设计决定,虽然我没有参与其中,但我可以肯定,它的目标是避免遍历基础集合时出现不一致的目的。最好以尽早引发异常并让用户知道,而不是以意外或不一致的数据结束。
但是,文档还提到快速失败迭代器会在尽力而为的基础上抛出ConcurrentModificationException
。
例如,考虑以下代码:
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
try {
Integer next = iterator.next();
System.out.println("iterator.next() = " + next);
System.out.println("BEFORE remove: " + list);
list.remove(0);
System.out.println("AFTER remove: " + list);
} catch (ConcurrentModificationException e) {
System.out.println("CME thrown, ending abnormally...");
System.exit(1);
}
}
在这里,我们尝试在遍历列表时删除列表的第一个元素。执行后,您将看到以下输出:
iterator.next() = 1
BEFORE remove: [1, 2, 3]
AFTER remove: [2, 3]
CME thrown, ending abnormally...
这是预期的行为。当迭代器第二次尝试从列表中获取元素时,将引发ConcurrentModificationException
。这是因为该实现检测到自创建迭代器以来已进行了结构更改。因此,iterator.next()
会引发CME。
但是,由于不能保证 fail-fast 迭代器总是抛出ConcurrentModificationException
,而是仅在尽力而为的基础上进行迭代,因此有一种方法可以欺骗实现。考虑下面的代码,它是上一个代码片段的修改版本(如果执行,请注意,直到完成该代码需要一些时间):
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
int structuralChanges = 0;
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
try {
Integer next = iterator.next();
System.out.println("iterator.next() = " + next);
System.out.println("BEFORE remove: " + list);
list.remove(0);
System.out.println("AFTER remove: " + list);
structuralChanges++;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
list.add(i);
structuralChanges++;
list.remove(list.size() - 1);
structuralChanges++;
}
list.add(0);
structuralChanges++;
System.out.println("Structural changes so far = " + structuralChanges);
} catch (ConcurrentModificationException e) {
System.out.println("CME NEVER thrown, so you won't see this message");
System.exit(1);
}
}
System.out.println("AFTER everything: " + list);
System.out.println("Program ending normally");
在这里,我们再次在迭代时删除列表的第一个元素,但是在迭代器获取下一个元素之前,我们正在做数百万个结构修改。实际上,如果我们考虑Integer.MAX_VALUE * 2 + 1
循环内的所有list.add(i)
和list.remove(list.size() - 1)
调用以及立即进行的for
调用,我们就在进行list.add(0)
结构修改在for
循环之后。
此外,我们通过structuralChanges
变量跟踪所有结构修改,该变量在每次结构修改后立即递增。
执行上述代码后,您将看到以下输出:
iterator.next() = 1
BEFORE remove: [1, 2, 3]
AFTER remove: [2, 3]
Structural changes so far = 0
iterator.next() = 3
BEFORE remove: [2, 3, 0]
AFTER remove: [3, 0]
Structural changes so far = 0
iterator.next() = 0
BEFORE remove: [3, 0, 0]
AFTER remove: [0, 0]
Structural changes so far = 0
AFTER everything: [0, 0, 0]
Program ending normally
如输出所示,没有抛出ConcurrentModificationException
。
此外,在完成所有结构修改后,structuralChanges
变量的值最终为0
。这是因为它是int
类型的变量,并且在被0
次(由于我们的人工{{1 }}循环并随后递增,由于调用了原始代码Integer.MAX_VALUE * 2 + 2
而加上了Integer.MAX_VALUE * 2 + 1
。
然后,在进行所有这些for
结构修改之后,当我们在1
循环的后续迭代中调用list.remove(0)
时,永远不会抛出Integer.MAX_VALUE * 2 + 2
。我们欺骗了实现,因此现在我们可以内部查看数据发生了什么。 (在内部,该实现通过保持iterator.next()
计数来跟踪结构修改,就像我对while
变量所做的那样。)