foreach循环中不能使用remove删除元素的原理解析
Posted ABin-阿斌
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了foreach循环中不能使用remove删除元素的原理解析相关的知识,希望对你有一定的参考价值。
声明:
- 原作者:掘金:https://juejin.cn/user/3940246036953293
- 原文链接:https://juejin.cn/post/6950438574164541454
前言
- 相信大家肯定都看过阿里巴巴开发手册,而在阿里巴巴开发手册中明确的指出,不要再 foreach 循环里面进行元素的 add 和remove,如果你非要进行 remove 元素,那么请使用 Iterator 方式,如果存在并发,那么你一定要选择加锁。
foreach
public static void main(String[] args)
List<String> list = new ArrayList<String>();
list.add("11");
list.add("22");
list.add("33");
list.add("44");
for (String s : list)
if ("22".equalsIgnoreCase(s))
list.remove(s);
System.out.println(JSONObject.toJSONString(list));
输出结果:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at org.example.list.Test01.main(Test01.java:22)
Process finished with exit code 1
分析异常:
final void checkForComodification()
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
-
比较两个值
modCount
和expectedModCount
,那么这两个变量是什么呢? -
其中
modCount
表示集合的修改次数,这其中包括了调用集合本身的add方法等修改方法时进行的修改和调用集合迭代器的修改方法进行的修改。而expectedModCount
则是表示迭代器对集合进行修改的次数。
先来看看反编译之后的代码,如下:
public static void main(String[] args)
List<String> list = new ArrayList();
list.add("11");
list.add("22");
list.add("33");
list.add("44");
Iterator var2 = list.iterator();
while(var2.hasNext())
String s = (String)var2.next();
if ("22".equalsIgnoreCase(s))
list.remove(s);
System.out.println(JSONObject.toJSONString(list));
- 看里面使用的也是迭代器,也就是说,其实 foreach 每次循环都调用了一次
iterator
的next()
方法, foreach方式中调用的remove
方法,是ArrayList内部的remove
方法,会更新modCount
属性
我们可以看看ArrayList类中的remove
方法
public E remove(int index)
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
-
看到此方法中,有一个
modCount++
的操作,也就是说,modCount
会一直更新变化。 -
我们第一次迭代的时候 11 != 22 ,直接迭代第二次,这时候就相等了,执行
remove()
方法,这时候就是modCount++
,再次调用next()
的时候,modCount = expectedModCount
这个就不成立了,所以异常信息出现了,其实也可以理解为在hasNext()
里面,cursor != size
而这时候就会出现错误了。 -
也就是说
remove
方法它只修改了modCount
,并没有对expectedModCount
做任何操作。
迭代器
为什么阿里巴巴的规范手册会这样子定义?
它为什么推荐我们使用 Iterator呢?
- 直接使用迭代器会修改
expectedModCount
,而我们使用foreach的时候,remove方法它只修改了modCount
,并没有对expectedModCount
做任何操作,而Iterator就不会这个样子。
public static void main(String[] args)
List<String> list = new ArrayList<String>();
list.add("11");
list.add("22");
list.add("33");
list.add("44");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext())
String item = iterator.next();
if("22".equals(item))
iterator.remove();
System.out.println(JSONObject.toJSONString(list));
输出结果:
["11","33","44"]
Process finished with exit code 0
可以看出结果是正确的,下面我们来分析一下:
先来看看反编译之后的代码:
public static void main(String[] args)
List<String> list = new ArrayList();
list.add("11");
list.add("22");
list.add("33");
list.add("44");
Iterator iterator = list.iterator();
while(iterator.hasNext())
String item = (String)iterator.next();
if ("22".equals(item))
iterator.remove();
System.out.println(JSONObject.toJSONString(list));
主要观察remove()
方法的实现,那么需要先看 ArrayList.class:
public Iterator<E> iterator()
return new Itr();
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E>
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
Itr()
public boolean hasNext()
return cursor != size;
public void remove()
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification(); //第一步
try
ArrayList.this.remove(lastRet); //第二步:调用list的remove方法
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount; //第三步:modCount是remove方法去维护更新,
//由于第一步中校验 modCount 和 expectedModCount 是否相当等
catch (IndexOutOfBoundsException ex)
throw new ConcurrentModificationException();
final void checkForComodification()
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
-
调用
checkForComodification()
方法,作用:判断modCount
和expectedModCount
是否相当; -
foreach 方式中调用的
remove
方法,是ArrayList内部的remove
方法,会更新modCount
属性; -
将更新后的
modCount
重新赋值给expectedModCount
变量。
Java8的新特性
public static void main(String[] args)
List<String> list = new ArrayList<String>();
list.add("11");
list.add("22");
list.add("33");
list.add("44");
list.removeIf("22"::equals);
System.out.println(JSONObject.toJSONString(list));
总结
- for-each循环不仅适用于遍历集合和数组,而且能让你遍历任何实现Iterator接口的对象;
- 最最关键的是它还没有性能损失。而对数组或集合进行修改(添加删除操作),就要用迭代器循环。
- 所以循环遍历所有数据的时候,能用它的时候还是选择它吧。
以上是关于foreach循环中不能使用remove删除元素的原理解析的主要内容,如果未能解决你的问题,请参考以下文章
不要在 foreach 循环里进行元素的 remove / add 操作
Java ArrayList在foreach中remove的问题分析