“我想在循环中删除列表中的元素——结果遇到了ConcurrentModificationException”。熟悉吗?
\n如果你曾经用Java编程,并试图在迭代过程中(例如在for-each循环中)从ArrayList或HashMap中删除元素,那么你很可能见过这个可怕的红色异常。它总是在最不合时宜的时候出现,仿佛Java在对你说:“嘿,你在干什么?我在遍历集合的时候,不准碰它!”
在Stack Overflow上,这个问题获得了数千次浏览。这并非偶然:问题看似简单,但背后隐藏着一整套优化和多线程的细微差别。在本文中,我们将深入探讨如何在Java循环中正确地从集合中删除对象,既不引发ConcurrentModificationException,又不损失性能。
开始吧!
\n\n为什么会发生ConcurrentModificationException?
\n简短的回答是:快速失败(fail-fast)迭代器。Java Collections Framework中的大多数集合(ArrayList、HashSet、HashMap等)都使用一个内部修改计数器(modCount)。当你创建一个迭代器时,它会记录当前的modCount值。每次调用next()时,迭代器都会检查集合是否被外部修改(通过集合自身的add()或remove()方法)。如果计数器不一致,就会抛出异常。
一个肯定会出错的代码示例:
\nList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));\nfor (String item : list) {\n if ("B".equals(item)) {\n list.remove(item); // ConcurrentModificationException!\n }\n}\n\n为什么?因为for-each隐式地使用了迭代器,而list.remove()直接修改了modCount,绕过了迭代器。迭代器不会容忍这种行为。
方法#1:使用Iterator.remove()——经典方法
\n最简单且正确的方法是显式地使用迭代器并调用其remove()方法。该方法会同步迭代器的内部计数器与集合。
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));\nIterator<String> iterator = list.iterator();\nwhile (iterator.hasNext()) {\n String item = iterator.next();\n if ("B".equals(item)) {\n iterator.remove(); // 安全!\n }\n}\nSystem.out.println(list); // [A, C, D]\n\n优点:安全,适用于所有支持迭代器的集合。缺点:略显冗长。对于简单的过滤,可以使用更现代的方法。
\n\n方法#2:Collection.removeIf()——现代且优雅
\n从Java 8开始,大多数集合都提供了removeIf(Predicate filter)方法。这是根据条件删除元素最简洁的方式。
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));\nlist.removeIf(item -> "B".equals(item));\nSystem.out.println(list); // [A, C, D]\n\n在内部,removeIf使用迭代器(或通过保留索引直接遍历),因此不会出现ConcurrentModificationException。对于简单情况,这是推荐的方法。
性能:对于ArrayList,removeIf方法的时间复杂度为O(n),但执行效率很高——它通过一次遍历移动元素,而不是逐个删除(后者时间复杂度为O(n²))。
方法#3:按索引反向遍历(适用于List)
\n如果出于某种原因你无法使用迭代器(例如,你需要同时修改多个元素,或者你正在处理旧代码),你可以使用普通的for循环按索引反向遍历列表。
List<String> list = new A