阿里的 Java 开发手册里会强制不要在 foreach 里进行元素的删除操作
在使用Iterator迭代器遍历并修改集合对象时,有时会抛出异常,例如HashMap、ArrayList等。有时不会抛出异常,例如ConcurrentHashMap等。其中就涉及到了快速失败(fail-fast)和安全失败(fail-safe)。
foreach
for-each 本质上是个语法糖,底层是通过迭代器 Iterator 配合 while 循环实现的。
private class Itr implements Iterator<E> {
int cursor; // 下一个元素的索引
int lastRet = -1; // 上一个返回元素的索引;如果没有则为 -1
int expectedModCount = modCount; // ArrayList 的修改次数
Itr() { } // 构造函数
public boolean hasNext() { // 判断是否还有下一个元素
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() { // 返回下一个元素
checkForComodification(); // 检查 ArrayList 是否被修改过
int i = cursor; // 当前索引
Object[] elementData = ArrayList.this.elementData; // ArrayList 中的元素数组
if (i >= elementData.length) // 超出数组范围
throw new ConcurrentModificationException(); // 抛出异常
cursor = i + 1; // 更新下一个元素的索引
return (E) elementData[lastRet = i]; // 返回下一个元素
}
}
expectedModCount 被赋值为 modCount,而 modCount 是 ArrayList 中的一个计数器,用于记录 ArrayList 对象被修改的次数。ArrayList 的修改操作包括添加、删除、设置元素值等。每次对 ArrayList 进行修改操作时,modCount 的值会自增 1。在迭代 ArrayList 时,如果迭代过程中发现 modCount 的值与迭代器的 expectedModCount 不一致,则说明 ArrayList 已被修改过,此时会抛出ConcurrentModificationException 异常。
这种机制可以保证迭代器在遍历 ArrayList 时,不会遗漏或重复元素,同时也可以在多线程环境下检测到并发修改问题。
总结:
- 因为 foreach 循环是基于迭代器实现的,而迭代器在遍历集合时会维护一个 expectedModCount 属性来记录集合被修改的次数。如果在 foreach 循环中执行删除操作会导致 expectedModCount 属性值与实际的 modCount 属性值不一致,从而导致迭代器的 hasNext() 和 next() 方法抛出 ConcurrentModificationException 异常。
- 为了避免这种情况,应该使用迭代器Iterator 的 remove() 方法来删除元素,该方法会在删除元素后更新迭代器状态,确保循环的正确性。如果需要在循环中删除元素,应该使用迭代器的 remove() 方法,而不是集合自身的 remove() 方法。
快速失败(fail-fast)
在使用迭代器对集合对象进行遍历时,如果A线程正在对集合进行遍历,此时B线程对集合进行修改(增加、删除、修改)操作,或者A线程在遍历过程中对集合进行修改,都会导致A线程抛出ConcurrentModificationException
异常。
注:可以用Iterator提供的remove()
方法安全的删除上一次返回的对象。
为什么在用迭代器遍历集合时,修改集合会抛出异常呢?
原因是迭代器遍历过程中使用了一个modCount变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hasNext()、next()方法遍历 下一个元素之前,都会检测modCount变量是否为exceptedModCount值,是的话就返回遍历;否则抛出异常,终止遍历。
安全失败(fail-safe)
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制一份原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合所做的修改并不能被迭代器检测到,所以不会抛出异常。
最后总结一下,快速失败和安全失败是对迭代器遍历集合而言的。并发环境下建议使用java.util.concurrent包下的容器类;除非没有修改操作。