【阿里规范】不要在 foreach 循环里进行元素的 remove/add 操作
这几天没事的时候把阿里的Java开发手册大概看了一遍,其中有一条如下:
【强制】不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。
正例:
Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if (删除元素条件) { iterator.remove(); } }
反例:
List<String> list = new ArrayList<String>(); list.add("1"); list.add("2"); for (String item : list) { if ("1".equals(item)) { list.remove(item); } }
说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?
这个规则我之前是知道的,但是看到上面这个说明,就有点不太淡定了,难道还会有什么奇葩的事不成。于是把这行代码考到IDEA运行了一下,正例自然是不必说。
果然remove
元素1
的时候正常,remove
2
报java.util.ConcurrentModificationException
异常。然后我又添加元素3
,这下情况又变了,remove
1
和3
异常,remove
2
正常。发现一个规律,remove
倒数第二元素正常,其他元素就会抛异常。
因为foreach
这种遍历的语法,本身就是Java的语法糖,我们要看真实的代码实现,上述反例经过Java编译器编译后实际上代码如下:
ArrayList list = new ArrayList(); list.add("1"); list.add("2"); Iterator i$ = list.iterator(); while(i$.hasNext()) { String item = (String)i$.next(); if("2".equals(item)) { list.remove(item); } }
简单的打了几个断点发现,问题和hasNext()
有很大关系。 看一下ArrayList的hasNext()的实现
public boolean hasNext() { return cursor != size; }
看ArrayList的源码,很多方法比如:next()
、previous()
、remove()
在执行前都会先执行以下方法:
final void checkForComodification() { if (expectedModCount != ArrayList.this.modCount) throw new ConcurrentModificationException(); }
简单的说checkForComodification()
是为了保证访问的一致性。 下面我们通过人肉执行循环来看一下这其中报异常或者不报异常的原因 remove
1
的循环如下:
步骤 | 代码 | 表达式值 | cursor | size | 备注 |
---|---|---|---|---|---|
1 | i$.hasNext() | true | 0 | 2 | |
2 | String item = (String)i$.next(); | - | 1 | 2 | |
3 | if("1".equals(item)) | true | 1 | 2 | |
4 | list.remove(item); | - | 1 | 1 | |
5 | i$.hasNext() | false | 1 | 1 | 此时循环结束,走不到下一个next() 因此也不会抛出异常 |
remove
2
的循环如下:
步骤 | 代码 | 表达式值 | cursor | size | 备注 |
---|---|---|---|---|---|
1 | i$.hasNext() | true | 0 | 2 | |
2 | String item = (String)i$.next(); | - | 1 | 2 | |
3 | if("2".equals(item)) | false | 1 | 2 | |
4 | i$.hasNext() | true | 1 | 2 | |
5 | String item = (String)i$.next(); | - | 2 | 2 | |
6 | if("2".equals(item)) | true | 2 | 2 | |
7 | list.remove(item); | - | 2 | 1 | remove 操作完成后modCount != expectedModCount ,因为没有执行到next() 方法,此时还不会抛出异常 |
8 | i$.hasNext() | true | 2 | 1 | |
9 | String item = (String)i$.next(); | - | 2 | 1 | 此时抛出异常 |
现在终于明白为什么remove
倒数第二个元素不报错的原因了,因为remove
倒数第二元素后刚好cursor == size
,此时hasNext()
返回false。
关于modCount
和expectedModCount
可参考如下资料:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具