避免Java中的ConcurrentModificationException

2023/06/07

1. 概述

在本文中,我们将介绍ConcurrentModificationException类。

首先,我们将解释它是如何工作的,然后通过使用触发它的测试来证明它。

最后,我们将通过实际示例尝试一些解决方法。

2. 引发ConcurrentModificationException

本质上,ConcurrentModificationException用于在我们正在迭代的内容被修改时快速失败,让我们通过一个简单的测试来证明这一点:

@Test(expected = ConcurrentModificationException.class)
public void givenIterating_whenRemoving_thenThrowException() {
    List<Integer> integers = newArrayList(1, 2, 3);

    for (Integer integer : integers) {
        integers.remove(1);
    }
}

如我们所见,在完成迭代之前,我们试图删除一个元素。这就是触发异常的原因。

3. 解决方案

有时,我们实际上可能希望在迭代时从集合中删除元素。如果是这种情况,那么有一些解决方案。

3.1 直接使用迭代器

for-each循环在幕后使用迭代器,但不那么冗长。但是,如果我们重构之前的测试以使用迭代器,我们将可以访问其他方法,例如remove()。让我们尝试使用此方法来修改我们的集合:

@Test
public void givenIterating_whenUsingIteratorRemove_thenNoError() {
    List<Integer> integers = newArrayList(1, 2, 3);

    for (Iterator<Integer> iterator = integers.iterator(); iterator.hasNext(); ) {
        Integer integer = iterator.next();
        if (integer == 2) {
            iterator.remove();
        }
    }

    assertThat(integers).containsExactly(1, 3);
}

现在我们会注意到没有发生异常。原因是remove()方法不会导致ConcurrentModificationException,迭代时调用是安全的。

3.2 迭代期间不删除

如果我们想保留我们的for-each循环,那也不是不可以,只是我们需要等到迭代之后再移除元素。让我们通过在迭代时将要删除的内容添加到toRemove集合来实现这一点:

@Test
public void givenIterating_whenUsingRemovalList_thenNoError() {
    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()方法。这意味着如果我们使用它,我们可以使用函数式编程的方式再次实现相同的结果:

@Test
public void whenUsingRemoveIf_thenRemoveElements() {
    Collection<Integer> integers = newArrayList(1, 2, 3);

    integers.removeIf(i -> i == 2);

    assertThat(integers).containsExactly(1, 3);
}

这种声明式风格为我们提供了最少的冗长。但是,根据用例,我们可能会发现其他方法更方便。

3.4 使用Stream过滤

当深入到函数式/声明式编程的世界时,我们可以忘记改变集合,相反,我们可以专注于应该实际处理的元素:

@Test
public void whenUsingStream_thenRemoveElements() {
    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上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章