前言
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家:https://www.captainbed.cn/z
1. 错误场景复现
场景1:增强型for循环删除元素
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
// 尝试删除包含 "B" 的元素
for (String s : list) { // 触发 ConcurrentModificationException
if ("B".equals(s)) {
list.remove(s); // 直接操作原集合
}
}
增强型for循环底层使用迭代器,直接调用List.remove()
会破坏迭代器状态。
场景2:索引遍历的遗漏问题
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
// 删除所有偶数
for (int i = 0; i < numbers.size(); i++) {
if (numbers.get(i) % 2 == 0) {
numbers.remove(i); // 删除后未调整索引,导致跳过元素
}
}
// 最终结果:[1, 3, 5]?错误!实际输出:[1, 3, 4](元素4被跳过)
正向遍历时删除元素会导致后续元素前移,索引未回溯引发逻辑错误。
场景3:多线程并发修改
List<String> dataList = new ArrayList<>(Arrays.asList("data1", "data2"));
// 线程1:遍历集合
new Thread(() -> {
for (String s : dataList) { // 可能触发ConcurrentModificationException
System.out.println(s);
Thread.sleep(1000); // 模拟耗时操作
}
}).start();
// 线程2:修改集合
new Thread(() -> {
dataList.add("data3"); // 修改集合结构
}).start();
ArrayList
非线程安全,多线程同时读写时可能抛出异常甚至导致数据损坏。
2. 原理解析
ConcurrentModificationException 的触发条件
// ArrayList.Itr 迭代器实现核心逻辑
int expectedModCount = modCount; // modCount记录集合结构修改次数
public boolean hasNext() {
return cursor != size;
}
public E next() {
checkForComodification(); // 关键检查:比较expectedModCount与modCount
// ... 其他逻辑
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
- modCount:集合结构变化(增/删)时自增的计数器
- expectedModCount:迭代器创建时记录的modCount快照
- 检测机制:任何遍历过程中修改集合的操作都会导致两者不一致
为什么允许通过迭代器删除?
// ArrayList.Itr.remove() 的实现
public void remove() {
// ... 前置检查
ArrayList.this.remove(lastRet); // 调用集合的remove方法
expectedModCount = modCount; // 手动同步计数器
// ... 其他逻辑
}
迭代器的remove()
方法会在操作后同步expectedModCount
,避免抛出异常。
3. 正确解决方案
方案1:显式使用迭代器删除(单线程场景)
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if ("B".equals(s)) {
iterator.remove(); // 安全删除当前元素
}
}
优势:
- 通过迭代器内部维护状态
- 支持动态集合修改
限制:
- 不适用于并发场景
- 只能删除当前遍历元素
方案2:使用Java 8的removeIf方法
list.removeIf(s -> "B".equals(s)); // 底层使用迭代器批量删除
// 等价于:
list = list.stream()
.filter(s -> !"B".equals(s))
.collect(Collectors.toList());
优势:
- 代码简洁,内部处理并发修改检查
- 支持Lambda表达式编写条件
性能注意:
- 需要创建新集合对象(Stream方式)
方案3:反向遍历删除(适用于索引遍历)
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i).contains("B")) {
list.remove(i); // 从后往前删除不会影响未遍历的索引
}
}
适用场景:
- 需要基于索引的复杂判断逻辑
- 无法使用迭代器的场景
方案4:并发安全集合(多线程场景)
// 使用CopyOnWriteArrayList(读多写少场景)
List<String> safeList = new CopyOnWriteArrayList<>(list);
for (String s : safeList) { // 迭代器持有旧数组快照
if ("B".equals(s)) {
safeList.remove(s); // 操作新数组,不影响遍历
}
}
// 或使用Collections.synchronizedList + 同步块
synchronized (syncList) {
Iterator<String> it = syncList.iterator();
while (it.hasNext()) {
String s = it.next();
if ("B".equals(s)) {
it.remove();
}
}
}
选型建议:
- CopyOnWriteArrayList:适合遍历频繁、修改少的场景(有内存开销)
- synchronizedList:适合写操作较多的场景(需手动同步)
4. 工具与最佳实践
静态分析工具配置
-
IntelliJ IDEA检测:
- 自动标记直接修改集合的代码
- 提示替换为
iterator.remove()
-
SonarQube规则:
"java:S2272"
- 检测非迭代器删除操作"java:S2114"
- 检查可能无效的集合批处理
Code Review检查清单
检查项 | 正确做法 |
---|---|
遍历时是否直接操作原集合? | 必须通过迭代器或并发安全集合修改 |
多线程环境是否使用线程安全集合? | 选择CopyOnWriteArrayList或ConcurrentHashMap |
是否误用subList() ? | 注意subList与原集合的修改相互影响 |
大数据量删除是否高效? | 优先使用removeIf() 或批量操作API |
5. 真实案例
某社交平台的消息队列处理服务中,开发人员使用以下代码清理过期消息:
for (Message msg : messageList) {
if (msg.isExpired()) {
messageList.remove(msg); // 直接调用remove
}
}
后果:
- 高峰时段触发
ConcurrentModificationException
,消息堆积导致服务宕机 - 累计丢失3万条未处理消息
修复方案:
- 替换为线程安全的
CopyOnWriteArrayList
- 使用并行流处理加速清理:
messageList.parallelStream() .filter(msg -> !msg.isExpired()) .collect(Collectors.toList());
总结
- 禁止在遍历中直接操作原集合:通过迭代器或专用API修改
- 线程安全是分层级的:单线程用迭代器,多线程用并发容器
- 善用Java 8+新API:
removeIf()
/Stream 简化代码且安全 - 逆向思维解决问题:反向遍历可规避索引错位问题
下期预告:《String拼接的代价:为何StringBuilder是救星?》——从JVM内存模型解析字符串操作的性能黑洞,揭秘JDK隐藏的优化技巧。
联系作者
职场经验分享,Java面试,简历修改,求职辅导尽在科技泡泡
思维导图面试视频合集