List可以一边遍历一边修改元素吗?
📌 核心答案
List能否边遍历边修改,取决于两个因素:
- 集合的实现机制(fail-fast 或 fail-safe)
- 遍历的方式(增强for、Iterator、索引遍历等)
🔍 详细分析
1. fail-fast集合(ArrayList、LinkedList)
1.1 什么是fail-fast?
- 定义:快速失败机制,在检测到并发修改时立即抛出异常
- 实现 :通过
modCount
计数器检测结构性修改 - 目的:避免在并发修改时产生不确定的行为
1.2 不同遍历方式的表现
❌ 增强for循环 - 不能修改
arduino
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
// 抛出 ConcurrentModificationException
for (String item : list) {
if ("B".equals(item)) {
list.remove(item); // ❌ 异常!
}
}
⚠️ Iterator遍历 - 有条件修改
vbnet
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("B".equals(item)) {
// list.remove(item); // ❌ 直接修改集合会抛异常
iterator.remove(); // ✅ 使用Iterator的remove方法
}
}
✅ 索引遍历 - 可以修改(需注意索引)
csharp
// 方式1:正序遍历(需要手动调整索引)
for (int i = 0; i < list.size(); i++) {
if ("B".equals(list.get(i))) {
list.remove(i);
i--; // 重要:调整索引
}
}
// 方式2:倒序遍历(推荐)
for (int i = list.size() - 1; i >= 0; i--) {
if ("B".equals(list.get(i))) {
list.remove(i); // 不需要调整索引
}
}
❌ Stream forEach - 不能修改
csharp
list.stream().forEach(item -> {
if ("B".equals(item)) {
list.remove(item); // ❌ ConcurrentModificationException
}
});
1.3 fail-fast原理解析
csharp
public abstract class AbstractList<E> {
// 修改次数计数器
protected transient int modCount = 0;
private class Itr implements Iterator<E> {
// 迭代器创建时记录的modCount
int expectedModCount = modCount;
public E next() {
checkForComodification(); // 每次调用都检查
// ... 返回元素
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
public void remove() {
// ... 执行删除
expectedModCount = modCount; // 同步modCount
}
}
}
2. fail-safe集合(CopyOnWriteArrayList)
2.1 什么是fail-safe?
- 定义:安全失败机制,允许在遍历时修改集合
- 实现:遍历的是集合的快照(副本)
- 特点:修改对当前遍历不可见
2.2 所有遍历方式都可以修改
✅ 增强for循环
csharp
List<String> list = new CopyOnWriteArrayList<>(Arrays.asList("A", "B", "C"));
for (String item : list) {
if ("B".equals(item)) {
list.remove(item); // ✅ 不会抛异常
list.add("D"); // ✅ 可以添加(但当前遍历看不到)
}
}
// 遍历看到: A, B, C
// 最终结果: [A, C, D]
✅ Iterator遍历
vbnet
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
if ("B".equals(item)) {
list.remove(item); // ✅ 可以
// it.remove(); // ⚠️ 注意:不支持Iterator.remove()
}
}
2.3 fail-safe原理解析
csharp
public class CopyOnWriteArrayList<E> {
private volatile Object[] array;
// 写操作:创建新数组
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
Object[] newElements = new Object[elements.length - 1];
// ... 复制数组,排除要删除的元素
setArray(newElements); // 替换引用
return oldValue;
} finally {
lock.unlock();
}
}
// 读操作:返回快照迭代器
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements Iterator<E> {
private final Object[] snapshot; // 持有数组快照
private COWIterator(Object[] elements, int initialCursor) {
snapshot = elements;
}
}
}
📊 对比总结表
特性 | fail-fast (ArrayList) | fail-safe (CopyOnWriteArrayList) |
---|---|---|
增强for循环修改 | ❌ 抛异常 | ✅ 可以 |
Iterator.remove() | ✅ 支持 | ❌ 不支持 |
直接修改集合 | ❌ 抛异常 | ✅ 可以 |
修改可见性 | 立即可见 | 对当前遍历不可见 |
性能 | 读写都快 | 写操作慢(复制数组) |
内存占用 | 低 | 高(维护副本) |
适用场景 | 单线程/读写均衡 | 读多写少的并发场景 |