1. 引言
在这篇文章中,我们将探讨java.util.ConcurrentModificationException
类的工作原理,并通过测试来验证它。接着,我们将通过实例展示一些避免此类异常的方法。
2. 触发ConcurrentModificationException
基本上,ConcurrentModificationException
会在遍历过程中对集合进行修改时触发一个快速失败。让我们用一个简单的测试来证明这一点:
@Test(expected = ConcurrentModificationException.class)
public void whilstRemovingDuringIteration_shouldThrowException() throws InterruptedException {
List<Integer> integers = newArrayList(1, 2, 3);
for (Integer integer : integers) {
integers.remove(1);
}
}
如图所示,在遍历结束前我们删除了一个元素,这就引发了异常。
3. 解决方案
有时,我们确实可能在遍历期间从集合中删除元素。这时,有一些解决方法。
3.1. 直接使用迭代器
for-each
循环背后使用了迭代器,但代码更简洁。如果我们把之前的测试改写成使用迭代器,我们将能够访问更多方法,如remove()
。让我们尝试使用这个方法来修改列表:
for (Iterator<Integer> iterator = integers.iterator(); iterator.hasNext();) {
Integer integer = iterator.next();
if(integer == 2) {
iterator.remove();
}
}
现在我们注意到没有异常。这是因为remove()
方法不会引发ConcurrentModificationException
,在遍历过程中调用它是安全的。
3.2. 遍历后才删除
如果我们想保留for-each
循环,可以这样做。只是我们需要在遍历结束后再删除元素。让我们尝试在遍历时将要删除的元素添加到toRemove
列表中:
List<Integer> integers = newArrayList(1, 2, 3);
List<Integer> toRemove = newArrayList();
for (Integer integer : integers) {
if(integer == 2) {
toRemove.add(integer);
}
}
integers.removeAll(toRemove);
assertThat(integers).containsExactly(1, 3);
这是另一种有效避免问题的方法。
3.3. 使用removeIf()
Java 8为Collection
接口引入了removeIf()
方法,这意味着如果我们在使用它,可以利用函数式编程的思想实现相同的效果:
List<Integer> integers = newArrayList(1, 2, 3);
integers.removeIf(i -> i == 2);
assertThat(integers).containsExactly(1, 3);
这种声明式风格提供了最少的冗余。然而,根据具体场景,我们可能会发现其他方法更方便。
3.4. 使用流过滤
当我们进入函数式/声明式编程的世界,可以忘掉直接修改集合,而是专注于实际需要处理的元素:
Collection<Integer> integers = newArrayList(1, 2, 3);
List<String> collected = integers
.stream()
.filter(i -> i != 2)
.map(Object::toString)
.collect(toList());
assertThat(collected).containsExactly("1", "3");
我们与之前示例相反,提供了一个确定要包含的元素的谓词,而非排除。优点是我们可以将其他函数与删除操作串联起来。在这个例子中,我们使用了函数式map()
,但如果需要,还可以使用更多操作。
4. 总结
在这篇文章中,我们展示了在遍历期间从集合中删除元素时可能遇到的问题,以及提供了一些解决方案来规避这些问题。这些示例的实现可以在GitHub上找到,这是一个Maven项目,可以直接运行。