CodeGym /课程 /JAVA 25 SELF /安全删除元素

安全删除元素

JAVA 25 SELF
第 28 级 , 课程 2
可用

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:支持上述所有方式(IteratorListIteratorremoveIf)。
  • Set:没有索引,但标准的 IteratorremoveIf 有效。
  • Map:按条件删除可使用对 entrySet() 的迭代器:
    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}
    
    在 Java 8+ 中,事情简单多了:
    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. 方法对比

做个小表格来巩固。我们的脑子就喜欢这种方式。

方式 支持自 简洁度 安全性 灵活性
for-each + remove()
Java 5+ - -
Iterator + remove()
Java 5+ + +
ListIterator
Java 5+ + ++
removeIf
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

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION