为什么阿里巴巴禁止在 foreach 循环里进行元素的 remove/add 操作
https://www.hollischuang.com/archives/3304
目录
增强for循环:依赖了while循环和Iterator
报错机制:fail-fast
报错原因:代码比较简单,modCount != expectedModCount的时候,就会抛出ConcurrentModificationException。
解决方法:使用 Iterator 本身的方法 remove() 来删除对象
public static void main(String[] args) {
// 使用ImmutableList初始化一个List
List<String> userNames = ImmutableList.of("Hollis", "hollis", "HollisChuang", "H");
System.out.println("使用for循环遍历List");
for (int i = 0; i < userNames.size(); i++) {
System.out.println(userNames.get(i));
}
System.out.println("使用foreach遍历List");
for (String userName : userNames) {
System.out.println(userName);
}
}
增强for反编译:
依赖了while循环和Iterator
Iterator iterator = userNames.iterator();
do
{
if(!iterator.hasNext())
break;
String userName = (String)iterator.next();
if(userName.equals("Hollis"))
userNames.remove(userName);
} while(true);
System.out.println(userNames);
增强for循环remove会报错?
之所以会出现这个异常,是因为触发了一个Java集合的错误检测机制——fail-fast 。
fail-fast
接下来,我们就来分析下在增强for循环中add/remove元素的时候会抛出java.util.ConcurrentModificationException的原因,即解释下到底什么是fail-fast进制,fail-fast的原理等。
fail-fast,即快速失败,它是Java集合的一种错误检测机制。当多个线程对集合(非fail-safe的集合类)进行结构上的改变的操作时,有可能会产生fail-fast机制,这个时候就会抛出ConcurrentModificationException(当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常)。
同时需要注意的是,即使不是多线程环境,如果单线程违反了规则,同样也有可能会抛出改异常。
那么,在增强for循环进行元素删除,是如何违反了规则的呢?
要分析这个问题,我们先将增强for循环这个语法糖进行解糖,得到以下代码:
public static void main(String[] args) {
// 使用ImmutableList初始化一个List
List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
Iterator iterator = userNames.iterator();
do
{
if(!iterator.hasNext())
break;
String userName = (String)iterator.next();
if(userName.equals("Hollis"))
userNames.remove(userName);
} while(true);
System.out.println(userNames);
}
我们直接看下checkForComodification方法的代码,看下抛出异常的原因:
Iterator是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出java.util.ConcurrentModificationException
异常。
使用 Iterator 本身的方法 remove() 来删除对象
所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 Iterator 本身的方法 remove() 来删除对象,Iterator.remove()
方法会在删除当前迭代对象的同时维护索引的一致性。
引用自:Java中的增强for循环(for each)的实现原理与坑
http://www.hollischuang.com/archives/1776
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
代码比较简单,modC
ount != expectedModCount
的时候,就会抛出ConcurrentModificationException
。
通过翻阅代码,我们也可以发现,remove方法核心逻辑如下:

可以看到,它只修改了modCount,并没有对expectedModCount做任何操作。
通过翻阅代码,我们也可以发现,remove方法核心逻辑如下:
private void fastRemove(int index) {
modCount++;
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
}
可以看到,它只修改了modCount,并没有对expectedModCount做任何操作。
简单画一张图描述下以上场景:

简单总结一下,之所以会抛出CMException异常,是因为我们的代码中使用了增强for循环,而在增强for循环中,集合遍历是通过iterator进行的,但是元素的add/remove却是直接使用的集合类自己的方法。这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除/添加了,就会抛出一个异常,用来提示用户,可能发生了并发修改!
所以,在使用Java的集合类的时候,如果发生CMException,优先考虑fail-fast有关的情况,实际上这里并没有真的发生并发,只是Iterator使用了fail-fast的保护机制,只要他发现有某一次修改是未经过自己进行的,那么就会抛出异常。