源码解析“不要在 foreach 循环里进行元素的 remove或add 操作”
前言
《阿里巴巴Java开发手册》里看到里面提到这样一条编程规约让我不理解,即:
【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
正例:
Iterator<String> it = a.iterator();
while (it.hasNext()) {
String temp = it.next();
if (删除元素的条件) {
it.remove();
}
}
反例:
List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
for (String temp : a) {
if ("1".equals(temp)) {
a.remove(temp);
}
}
说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的 结果吗?
跟着提供的正例反例,我进行测试,得到运行结果如下:
- 正例中remove操作都不会报错;
- 反例中remove"1"不会报错,当if语句中"1"换成"2"之后,则报java.util.ConcurrentModificationException异常
运行结果还真是挺有意思,我不理解为什么会出现这样的现象,于是便找下真正的原因。
例子展示
为了方便理解,我还是以ArrayList为例,代码重新组织了下,写了个Demo类(JDK1.8),代码如下:
public class ListMod {
public static void main(String[] args) {
String removeEle = "1";
iteratorRemove(removeEle);
foreachRemove(removeEle);
}
/**
* 用迭代器操作移除
* */
public static void iteratorRemove(String removeEle){
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (removeEle.equals(item)) {
iterator.remove();
}
}
}
/**
* foreach里进行移除
* */
public static void foreachRemove(String removeEle){
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
for (String item : list) {
if (removeEle.equals(item)) {
list.remove(item);
}
}
}
}
测试将removeEle分别取"1"、“2”、“3”、“4”,得到测试结果如下:
参数\结果 | 用迭代器操作移除 | foreach里进行移除 |
---|---|---|
“1” | 正常 | ConcurrentModificationException异常 |
“2” | 正常 | ConcurrentModificationException异常 |
“3” | 正常 | 正常 |
“4” | 正常 | ConcurrentModificationException异常 |
是不是非常奇怪,上述代码为什么偏偏removeEle=3的时候,不会报异常?
源码解析
首先,可以根据代码编辑器控制台错误栈信息来看下报错的位置,是在ArrayList的私有内部类Itr的checkForComodification方法中报出的错误
private class Itr implements Iterator<E> {
/****** 省略其他代码 ******/
/**
* 检查并发修改 Concurrency Modification
* 主要是用来实现fail-fast机制,阻止多线程的并发操作
*/
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
错误的原因是由于属性modCount != expectedModCount导致的,那这里有个问题,明明是foreach循环,为什么会使用到Itr里的checkForComodification方法呢?
foreach是个语法糖,我们无法直接点进某个方法查看遍历方法的实现。但是我们进行反编译看看。我用例子ListMod.class用JAD反编译得到结果文件ListMod.jad。打开这个jad文件,可以看到foreachRemove反编译之后的结果代码为:
public static void foreachRemove(String removeEle)
{
List list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
Iterator iterator = list.iterator();
do
{
if(!iterator.hasNext())
break;
String item = (String)iterator.next();
if(removeEle.equals(item))
list.remove(item);
} while(true);
}
上述代码可以看出,foreach循环本质上也是使用iterator迭代器,再看iterator()方法,其实也是返回上面提到的ArrayList内部类Itr对象
public Iterator<E> iterator() {
return new Itr();
}
回到上面报错的原因来,既然已经知道foreach的本质(可以看到无论是例子中的iteratorRemove方法或是foreachRemove方法,都其实用到了Itr中的hasNext和next方法),那就可以在Itr中hasNext和next打上断点,进行调试。
通过调试,最终可以确定foreachRemove里导致的报错,都是在Itr的next方法里调用的checkForComodification()方法被检查出异常,而当removeEle="3"时,移除"3"这个元素后(List倒数第二个元素),结合上面foreachRemove反编译后的代码,可以看出下一次循环中执行hasNext()方法返回false(cursor=size,至于为什么,继续看下面的remove(Object o)方法),因此它跳出了do-while的循环,便也不会执行后面的代码。
OK,既然根据调试已经找到了例子结果产生的原因,但是为了加深理解,还是要理解下这些相关的方法和变量的含义。先来看上面提到比较重要的Itr中的hasNext()和next()以及其相关的属性cursor和lastRet(其他代码暂时先省略)
private class Itr implements Iterator<E> {
// index of next element to return 之后调用next返回的元素索引
int cursor;
// index of last element returned; -1 if no such 最近一次返回的元素索引。没有遍历前,值为-1
int lastRet = -1;
/****** 省略其他代码 ******/
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
// 取得到next对应的索引
int i = cursor;
// 没有>=数组大小的元素索引
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
// 判断i是否大于实际的数组长度
if (i >= elementData.length)
throw new ConcurrentModificationException();
// 光标+1
cursor = i + 1;
// 返回元素
return (E) elementData[lastRet = i];
}
/****** 省略其他代码 ******/
}
从上述代码中可以看出,next()就是通过光标cursor不断向下一个移动进行元素获取,而lastRet则记录其最后一个移动获取到的元素索引。
再来看看和Remove操作相关的代码:
private class Itr implements Iterator<E> {
// index of next element to return
int cursor;
// index of last element returned; -1 if no such
int lastRet = -1;
// 迭代器认为支持 List 应该具有的 modCount 值。如果违反期望,如果违反了此预期,迭代器将检测到并发修改
int expectedModCount = modCount;
/****** 省略其他代码 ******/
public void remove() {
// lastRet < 0 不允许使用remove
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
// 移除最后遍历到lastRet对应元素,remove方法里会更改modCount
ArrayList.this.remove(lastRet);
// 光标 = lastRet
cursor = lastRet;
// lastRet置为-1,可阻止光标未再次移动时,重复进行remove
lastRet = -1;
// 期望修改次数等于修改次数
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
/****** 省略其他代码 ******/
}
/****** 省略不必要的代码 ******/
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
* 删除此list中指定位置的元素。将任何后续元素向左移动(从其索引中减去一个)。
* @param index the index of the element to be removed 参数:要被移除元素的索引
* @return the element that was removed from the list 返回:从list中移除的元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
rangeCheck(index);
// 修改次数+1
modCount++;
// 获取该index对应的旧值
E oldValue = elementData(index);
// 拷贝时需要移动的个数 = 数组长度 - index -1
int numMoved = size - index - 1;
// 若移动的个数>0,则将index后的所有元素相当于都移到前面一个位置上
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 由于上面做了移动,则最后一个元素置为null(也兼容移除最后一个元素不进上面numMoved > 0判断的操作)
elementData[--size] = null; // clear to let GC do its work
// 返回移除的元素
return oldValue;
}
// 省略不必要的代码
/**
* Removes the first occurrence of the specified element from this list,
* if it is present. If the list does not contain the element, it is
* unchanged. More formally, removes the element with the lowest index
* <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>
* (if such an element exists). Returns <tt>true</tt> if this list
* contained the specified element (or equivalently, if this list
* changed as a result of the call).
* 从该列表中删除第一个出现的指定元素(如果存在)。如果列表中不包含该元素,它将保持不变。更正式地说,删除元素值对应index最小的元素
*
* @param o element to be removed from this list, if present 要从此列表中删除的元素(如果存在)
* @return <tt>true</tt> if this list contained the specified element 如果此列表包含指定的元素,返回true
*/
public boolean remove(Object o) {
if (o == null) {
// 若要删除的元素值为null,则遍历elementData数组,找第一个等于null的元素进行移除
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 遍历elementData数组,找第一个与元素值相等的index进行删除
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
/****** 省略不必要的代码 ******/
/**
* Checks if the given index is in range. If not, throws an appropriate
* runtime exception. This method does *not* check if the index is
* negative: It is always used immediately prior to an array access,
* which throws an ArrayIndexOutOfBoundsException if index is negative.
* 检查给定index是否在范围内。如果不是,则抛出响应的运行时异常。
* 此方法不会检查索引是否为负:它总是在紧接在数据访问之前使用,如果索引为负,则会引发ArrayIndexOutOfBoundsException。
*/
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/****** 省略不必要的代码 ******/
/*
* Private remove method that skips bounds checking and does not return the value removed.
* 私有的remove方法,该方法跳过边界检查,不返回移除的值。
*/
private void fastRemove(int index) {
// 该方法逻辑与remove(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
}
根据上述代码,可以看到例子中使用的方法:
- iteratorRemove(Itr中remove() → remove(int index) ),会使modCount+1;
- foreachRemove(remove(Object o) → fastRemove(int index)),也会使modCount+1。
而这两者的区别,则是Itr中remove()中有"expectedModCount = modCount"的代码设置。因此采用迭代器Iterator移除的方式可以通过checkForComodification()的检查。
再说下上面提到的例子中foreachRemove(“3”)不会报错的具体原因:因为在判断进倒数第二个元素进行移除时,执行fastRemove(int index)中"elementData[–size]=null"代码会让size = size-1,那对应最后一个元素的cursor就刚好等于此时的size了。