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项目,可以直接运行。