zl程序教程

您现在的位置是:首页 >  其他

当前栏目

问题深究01——为什么不要在foreach循环里进行元素的remove/add操作?

2023-03-31 11:00:45 时间

不要在foreach循环里进行元素的remove/add操作。 remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。

  • 正例
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if (删除元素的条件) {
    	iterator.remove();
    }
}
  • 反例
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
    if ("1".equals(item)) {
    	list.remove(item);
    }
}

说明: 以上代码的执行结果肯定会出乎大家的意料,那么试一下把"1"换成"2",会是同样的结果吗?

上面这一段摘自《Java开发手册-嵩山版》编程规约-集合处理-第14条,最后一行的说明并没有给出答案。因此自己琢磨这验证一下。

代码验证

  • 尝试移除元素"1"
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
    if ("1".equals(item)) {
    	list.remove(item);
    }
}
System.out.println(list); //输出[2]
  • 尝试移除元素"2"
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
    if ("2".equals(item)) {
    	list.remove(item);
    }
}
System.out.println(list);

image

这里涉及到一个语法糖-增强for循环,从错误日志中可以看出增强for循环遍历list时涉及到ArrayList$Itr。

上述代码经过编译器处理后的class文件进行反编译,得到如下的代码片段

List<String> list = new ArrayList();
list.add("1");
list.add("2");
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
    String item = (String)iterator.next();
    if ("2".equals(item)) {
        list.remove(item);
    }
}

问题定位

通过日志定位找到抛异常的代码

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

从上述这段代码可以得出当modCount!=expectedModCount导致抛出的异常。

摘录ArrayList$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 E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
    }
}

查看ArrayList$Itr可以发现expectedModCount在创建Itr时通过modCount进行赋值,除此之外只有在remove()方法中重新通过modCount进行赋值。

重新查看测试代码后,摘录ArrayList中的相关代码

public Iterator<E> iterator() {
    return new Itr();
}
public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
        }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
        }
    }
    return false;
}
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
}

通过上述这段代码可以定位到问题了,在调用list.remove()->remove()->fastRemove()中修改了modCount,导致下一次调用next()方法时报异常
image

分析到这里细心的朋友可能又有一个新的问题了,元素"2"明明已经是list中的最后一个节点了,为什么还会再次调用到next()方法呢?

答案是list.remove()操作后list的size值会减1,而迭代器的hasNext()是通过cursor和size是否相等来判断是否还有下一个元素的。
image

至此,移除元素"2"报错的原因就分析完毕了。下面开始分析移除元素"1"时,为什么程序没有报异常?

List<String> list = new ArrayList();
list.add("1");
list.add("2");
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
    String item = (String)iterator.next();
    if ("1".equals(item)) {
        list.remove(item);
    }
}

可以很容易的发现两段代码的区别在于移除的元素在list中所处的位置不一样。经过对移除元素"2"的代码分析可以得知,问题的症结在于list.remove()方法修改modCount参数。所以我们还是从这个方法开始分析。

image

通过上述代码可以发现fastRemove()的原理是通过数组拷贝的方式将后一个元素的值拷贝到当前元素所在的位置。拷贝结束后尾节点置null,size-1。

在下一次调用hasNext()时结果为false导致迭代结束。

image

通过打印元素可以发现,移除元素"1"以后,元素"2"并没有被打印出来。
image

总结

  • 此问题出现的原因就是调用了list的remove(Object)方法而不是采用迭代器itr.remove()方法进行元素移除。
  • 遍历集合时需要对集合进行增删操作,统一采用迭代器的方式进行。
  • 所有的元素操作都是通过迭代器进行的,因此要进行并发操作时对迭代器加锁是比较合适的一种手段。