一、背景
在以前的随笔中说道过 ArrayList 的 foreach 迭代删除的问题: ArrayList 迭代过程删除问题
按照以前的说法,在 ArrayList 中通过 foreach 迭代删除会抛异常:java.util.ConcurrentModificationException
但是下面这段代码实际情况却没报异常,是什么情况?
- List < String > list = new ArrayList < String > ();
- list.add("1");
- list.add("2");
- for (String item: list) {
- System.out.println("item:" + item);
- if ("1".equals(item)) {
- list.remove(item);
- }
- }
二、分析
我们知道 ArrayList 的 foreach 迭代调用的是 ArrayList 内部类 Itr,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;
- public boolean hasNext() {
- return cursor != size;
- }
- @SuppressWarnings("unchecked")
- 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();
- }
- }
- final void checkForComodification() {
- if (modCount != expectedModCount)
- throw new ConcurrentModificationException();
- }
- }
按照调用顺序查看源码
1.hasNext
ArrayList 的 foreach 迭代首先调用的是 ArrayList 内部类 Itr 的 hasNext(),该方法会对当前循环指针和长度做判断。只有当 hasNext() 返回 true 才会执行 foreach 里面的代码,cursor = size 时候就退出循环(因为这两者相等意味着都遍历完了,假如 ArrayList 的 size=2,那么 hasNext 会被调用 3 次,但是第 3 次调用不会执行 foreach 里面代码)。
2. 如果上一步返回 true 的话会执行 Itr 的 next(),如果数据无异常的话 cursor = i + 1; 所以没执行一次 next() 时 cursor 都会 + 1
3. 接着会执行到 list.remove(item), 此处调用的是 ArrayList 的 remove(Object o) 而不是 Itr 的,看下 ArrayList 的 remove() 的源码:
- 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;
- }
o != null, 会进入 else 的 fastRemove(index); 可以看到 ArrayList 根据传入的值删除会进行遍历 equals 判断, 找到索引再通过 fastRemove(index) 删除,因此 List 频繁做删除修改效率比较低。
4. 再看下 fastRemove() 源码:
- * /
- 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
- }
第 8 行会把该索引对应的数组的值置为 null, 并且 size-1。但是却没有进行 cursor - 1 操作
至此明白了。此处的 ArrayList 通过 foreach 迭代删除为什么不会报错:
刚开始 ArrayList 的 size=2 时, cursor =0
①第一次 hasNext() 进来, cursor != size 进入 next() 后 cursor=1, 接着因为满足条件删除的时候 size-1=1;
②第二次 hasNext() 进来,cursor = size = 1,所以不会执行 foreach 的代码,也不会出现后面检测 modCount 值抛 ConcurrentModificationException
上述未抛异常的情况主要是 hasNext() 中判断遍历完成的条件与 ArrayList 删除后的数据刚好吻合而已。
所以只要满足条件:删除的元素在循环时的指针 cursor+1=size 就会出现这种情况!删除 ArrayList 倒数第二个(即第 size - 1 个元素)就会出现不抛异常的假象。
(例如 size=3, 删除第 2 个元素;size=4, 删除第 3 个元素)
因为删除后 size-1=cursor
public boolean hasNext() {
return cursor != size;
}
hasNext() 会返回 false, 不会进入 ArrayList 的迭代器,就不会进入 next() 执行 checkForComodification()
这是一种条件判断下的特殊情况,其他情况都会抛出异常,所以不要在 foreach 进行删除数据。请在迭代器中进行删除。
来源: http://www.bubuko.com/infodetail-2435978.html