看似正常的遍歷,為什么必崩?ConcurrentModificationException 其實早有預警!
看似正常的遍历,为什么必崩?ConcurrentModificationException 其实早有预警!
唉,这事儿说来话长。你有没有那种很奇怪的时刻:代码明明一切正常,编译都OK,跑着跑着,忽然蹦出来个叛徒,报了个“你并发修改啦!”?
其实,这种大坑就像地铁上的小偷,看不见、摸不着,一不小心钱包(程序)就没了。
写Bug如偷菜,一不留神全村通缉
事情的起因是这样的。
有一天我在整理公司老项目的代码,小需求一个,用 Java 给 ArrayList 加点新花样,比如遇到什么元素就直接剔除,边遍历边改。
想都没想,愣头青一样地来了这么一出:
for (String s : list) {
if ("badGuy".equals(s)) {
list.remove(s);
}
}
乍一看,多清爽,多洁癖。
代码逻辑一点毛病都没有,说的就是你——偷偷前端传的“badGuy”字符串,哥哥一个个揪出来送走!
结果呢?啪嚓,ConcurrentModificationException 闪亮登场!
名字这么长,按理说应该很厉害,其实是某种“你咋又动我家的东西”的小心眼……
踩坑瞬间
谁能想到,遍历个列表也能踩雷?
印象里 Java 的集合都很友善,管你怎折腾,顶多 IndexOutOfBoundsException 警告你别乱来。可这货不讲武德,莫名其妙就开始报ConcurrentModificationException。当时心情:
- “明明是自己家自己扫地,怎么房东出来踹我了?”
- “不是说增强for很安全么?”
- “难道remove要过于暴力了?”
用一句表格来总结下常见的“死亡操作”:
| 操作场景 | 结果 |
|---|---|
| for-each边遍历边删 | ConcurrentModificationException |
| 普通for配合remove | 可能正常, 可能下标错乱 |
| 迭代器remove | 香蕉皮,滑~~(恩,其实安全) |
还有最骚的:居然有时候不报错,数据还删错、乱序! 这得多可怕,测不出来,出BUG全靠缘分。
幕后黑手:modCount,你藏哪儿了?
其实,这背后有个小机关:集合内部的 modCount 变量。
你别看ArrayList的体面,其实心里住着个会记仇的小本本:只要你每增删一次,它modCount自增一次。
而for-each循环本质是用迭代器(Iterator)。
每次 next() 都偷看一下自己记的小本本,和外面对比一下,不一样就:嘿,抓住你了!
大概底层差不多就是这样:
public E next() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 正常流程……
}
所以你在循环时 list.remove(),迭代器发现你背着它搞事情,直接开除你!
东施效颦式“修复”与正道之光
那怎么办?刺客都被抓包了,程序还能跑吗?
搜百度,大佬们早给方案了:
- 乖乖用Iterator的remove():
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if ("badGuy".equals(it.next())) {
it.remove();
}
}
别看少了一个点,BUG都绕着你走。(可惜没人用,面试会问)
-
倒序遍历:
这年头,谁还不会下楼?因为倒着删不会影响下一个元素的下标。 -
用专用的并发集合(比如CopyOnWriteArrayList):
土豪用法,不过性能也分场合,不然代码一夜回到解放前。
经验启示
唉,写了那么多年 Java,总有瞬间觉得自己像个学艺未精的武林小弟。
几句血泪总结:
- 不要在增强for循环或者foreach Lambda里直接改原集合,抓拍率99%。
- 改数据结构就用迭代器,别偷懒。
- 场合选并发安全方案,业务不急别CopyOnWriteArrayList。
- 不是每次异常都秒懂,调试能力贵过写代码本身。
说起来,还有一种阴间用法:“先收集要删的元素,循环结束后再统一remove”。恩,治标不治本,只能应急,不推荐!
共同學習,寫下你的評論
評論加載中...
作者其他優(yōu)質(zhì)文章