1. ConcurrentModificationException 的问题
本讲几乎不会有新知识点。但它非常重要,因为不小心删除数据往往会造成不可挽回的错误——尤其是在生产环境中。而且有一个有趣的类,你很可能会爱上它!
所以是的,再强调一次又一次:删除时绝对不要用 for-each!无论你多喜欢它。
我们先从一个经典示例开始,它让很多新手(甚至老手)都感到痛苦:
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
// 尝试删除所有偶数
for (Integer n : numbers) {
if (n % 2 == 0) {
numbers.remove(n); // 砰!ConcurrentModificationException
}
}
看起来好像应该能正常工作,但实际上程序会抛出异常:
Exception in thread "main" java.util.ConcurrentModificationException
现在我们详细看看发生了什么。当你通过 for-each(或普通的 Iterator)遍历集合时,集合内部会维护一个专门的“修改计数器”。如果在遍历期间集合被不是通过迭代器自身的方式修改,这个计数器会检测到“外部修改”,并抛出异常。这是一种防护机制,避免程序在已损坏的数据结构上工作。
2. 使用 Iterator
遍历时如何正确删除元素?
提醒一下:Iterator 是一个专门用于遍历集合并能“就地”安全删除元素的对象。它就像服务员,不仅能端菜,还能在绕桌时顺手把盘子收走。
获取迭代器
Iterator<Integer> it = numbers.iterator();
使用 while 遍历,并通过 it.remove() 删除
这是从列表中删除所有偶数的正确方式:
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
Integer n = it.next();
if (n % 2 == 0) {
it.remove(); // 安全删除当前元素
}
}
System.out.println(numbers); // [1, 3, 5]
要点:只能通过迭代器本身(it.remove())删除元素,且必须在调用 it.next() 之后。如果连续两次调用 remove() 而没有 next(),会得到 IllegalStateException。
3. ListIterator:更强大的能力
这就是开头提到的新东西!ListIterator 是用于列表(List)的“进阶”迭代器,它不仅能删除,还能在遍历时添加元素,并且可以双向移动(前进和后退)。
与普通 Iterator 的区别
- Iterator —— 直来直去:只能向前,只能删除。
- ListIterator —— 灵活机动:可前后移动,能删除、能通过 add() 添加,还能用 set() 替换当前元素。
示例:删除与添加元素
List<String> words = new ArrayList<>(List.of("cat", "dog", "bird"));
ListIterator<String> it = words.listIterator();
while (it.hasNext()) {
String word = it.next();
if (word.length() == 3) {
it.remove(); // 删除长度为 3 的单词
it.add("pet"); // 紧接着在被删除的位置后添加 "pet"
}
}
System.out.println(words); // [pet, pet, bird]
注意:通过 it.add() 添加时,元素会被插入到迭代器当前位置之后。
4. 使用 removeIf 删除
自 Java 8 起,出现了简洁易用的 removeIf 方法。它接受一个 lambda 表达式(或任意 Predicate),删除所有使条件返回 true 的元素。
示例:删除所有偶数
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
numbers.removeIf(n -> n % 2 == 0);
System.out.println(numbers); // [1, 3, 5]
这不仅更简洁,而且是安全的:方法内部使用了正确的迭代器——不会出现 ConcurrentModificationException。
示例:删除长度小于 3 的字符串
List<String> words = new ArrayList<>(List.of("hi", "cat", "no", "elephant"));
words.removeIf(word -> word.length() < 3);
System.out.println(words); // [cat, elephant]
建议:如果只是按条件删除元素——请使用 removeIf。这是最简洁、最现代的方式。
5. 实用建议
该选哪种方式?
- 如果需要按条件(即便较复杂)删除,且你使用的是 Java 8+:使用 removeIf —— 简洁、清晰、安全。
- 如果处在旧版 Java,或需要更复杂的遍历逻辑:使用 Iterator 及其 remove() 方法。
- 如果操作的是 List,并且希望遍历时不止删除还能添加元素:使用 ListIterator。
不同集合类型的注意点
- List:支持上述所有方式(Iterator、ListIterator、removeIf)。
- Set:没有索引,但标准的 Iterator 与 removeIf 有效。
- Map:按条件删除可使用对 entrySet() 的迭代器:
在 Java 8+ 中,事情简单多了:Map<String, Integer> map = new HashMap<>(Map.of("a", 1, "b", 2, "c", 3)); Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, Integer> entry = it.next(); if (entry.getValue() % 2 == 0) { it.remove(); } } System.out.println(map); // {a=1, c=3}map.entrySet().removeIf(entry -> entry.getValue() % 2 == 0);
6. 实战示例:用户过滤
假设我们有一个用户列表,想要删除所有未满 18 岁的用户。
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
List<User> users = new ArrayList<>(List.of(
new User("Anya", 17),
new User("Boris", 20),
new User("Vika", 15),
new User("Gleb", 25)
));
// 通过 removeIf 删除未成年人
users.removeIf(user -> user.age < 18);
System.out.println(users); // [Boris (20), Gleb (25)]
7. 方法对比
做个小表格来巩固。我们的脑子就喜欢这种方式。
| 方式 | 支持自 | 简洁度 | 安全性 | 灵活性 |
|---|---|---|---|---|
|
Java 5+ | - | ❌ | - |
|
Java 5+ | + | ✅ | + |
|
Java 5+ | + | ✅ | ++ |
|
Java 8+ | ++ | ✅ | + |
8. 从集合中删除元素的常见错误
错误 1:在 for-each 中尝试删除元素
for (String s : list) {
if (s.equals("test")) {
list.remove(s);
}
}
你已经知道:这样做不行——会得到 ConcurrentModificationException!请使用迭代器或 removeIf。
错误 2:在未调用 next() 的情况下调用迭代器的 remove()
Iterator<String> it = list.iterator();
it.remove(); // IllegalStateException — 在调用 next() 之前不能删除
错误 3:尝试从不可变集合中删除元素
List<String> immutable = List.of("a", "b", "c");
immutable.removeIf(s -> s.equals("a")); // UnsupportedOperationException
不可变集合不支持删除方法。
错误 4:尝试通过 values() 或 keySet() 从 Map 中删除元素且不使用迭代器
for (String key : map.keySet()) {
if (key.startsWith("a")) {
map.remove(key); // ConcurrentModificationException!
}
}
请使用对 entrySet() 的迭代器或 removeIf。
GO TO FULL VERSION