哈喽,大家好呀!我是小米,今天咱们来聊聊 Java 的 List 遍历和删除那些事儿。这方面其实有挺多坑,特别是并发场景下的小细节更是容易忽略。对了,今天我们要深入探讨两个很重要的机制——快速失败(fail-fast)和安全失败(fail-safe)。它们在遍历和删除元素时表现出的行为大有不同,尤其是在多线程环境下影响重大!
普通 for 循环删除 List 指定元素最经典的操作,大家应该都用过普通的 for 循环遍历一个 List 来删除指定的元素。然而,简单粗暴的 for 循环却并不适用于所有情况,特别是涉及并发或动态修改结构时就容易出问题。
举个例子,我们想从一个List中删除所有偶数元素:
为什么要 i-- 呢?
因为 List 是动态的数据结构,每次删除操作会让后续的元素往前移动一格,这会导致我们的索引不再准确,容易跳过元素。试想一下,如果删除了索引 1 的元素 2,那么原本在索引 2 的元素 3就会移到索引1 上去。如果没有减一操作,循环直接跳到下一个索引,3就会被跳过。
小结
普通 for 循环适合简单的删除操作,但是在多线程和并发场景中,普通 for 循环删除元素会带来一些不安全性问题,这里就需要了解 fail-fast 机制啦。
使用迭代器遍历并删除元素接下来,我们看看迭代器的操作。List 的 Iterator 是更常见的遍历方式,并且可以在遍历时安全删除元素——但前提是你得用对方法哦!
例如:
在迭代器中,remove()方法是安全的。迭代器会维护集合的结构变化(modCount),所以在遍历期间不会抛出 ConcurrentModificationException 异常。
注意
在 Iterator 遍历中,直接对 List 调用 remove(i) 方法会触发 ConcurrentModificationException,因为迭代器无法跟踪通过 List 的直接删除操作。下面的代码就是个经典反例:
foreach 循环删除元素foreach 循环是一种简洁的写法,不过它也存在一些陷阱。即便在Java 8 引入 forEachRemaining 方法后,foreach 依然无法实现边遍历边删除。
foreach 实质上是一个语法糖,底层依旧使用迭代器遍历,但不支持安全删除。为了避免异常,可以考虑先遍历收集要删除的元素,然后再进行批量删除。
快速失败(fail-fast)机制这里要着重讲一下快速失败的机制了!fail-fast 在 Java 中主要用于检测集合在并发修改下的结构性变化。在遍历过程中,如果结构发生了变化,例如删除了元素,Java 会立刻抛出 ConcurrentModificationException 异常。
fail-fast机制的背后是通过一个modCount 变量来实现的。每次集合结构发生变化时,modCount 的值会递增。迭代器在遍历时会检查 modCount是否变化。如果变化了,说明集合被修改,就会立刻触发 ConcurrentModificationException。
这种机制的好处是让程序及时发现问题,避免在集合状态不一致的情况下继续运行。但是也有局限性——它不适合并发环境。如果你必须在并发场景下安全操作 List,就需要了解 fail-safe机制。
安全失败(fail-safe)机制那么,什么是安全失败(fail-safe)机制呢?
fail-safe 机制不同于fail-fast,它不会直接访问原集合,而是会先创建一个集合的副本,迭代时操作副本内容,这样即便原集合被修改了也不会影响到当前遍历。不过,这种方式的缺点是,遍历期间集合的修改无法被同步感知。java.util.concurrent包下的许多集合类(如 CopyOnWriteArrayList 和 ConcurrentHashMap)都使用了fail-safe 机制。
示例
在这里,CopyOnWriteArrayList 采用了 fail-safe机制,允许我们在遍历期间删除元素,不会抛出 ConcurrentModificationException。但要注意:fail-safe 并发容器会在修改时消耗较多内存,因为它会创建副本。
使用场景
在高并发场景下,我们推荐使用fail-safe容器,比如 CopyOnWriteArrayList、ConcurrentHashMap等。它们的 fail-safe特性不仅可以避免异常抛出,而且能够确保在多线程环境下操作安全。但由于会创建副本,fail-safe更适合读多写少的场景,否则内存和性能消耗会非常大。
END我们来个小总结,看看这些遍历和删除操作的优缺点:
希望大家读完这篇文章,对 List 的遍历和删除有个更清晰的认识!如果你正在设计一个并发系统,选择 fail-fast还是 fail-safe机制的集合类,将会显著影响系统的稳定性和性能。下次我们继续深入 Java 并发编程的更多实战小技巧,我们不见不散!
我是小米,一个喜欢分享技术的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!