Java集合为什么不能在foreach中add/remove元素--全网最详细版

先来看一个例子:

public class TestFastFail {
    public static void main(String[] args) {
        testListRemove();
    }

    private static void testListRemove() {
        List<String> list = new ArrayList<>();
        list.add("zhuiangtao");
        list.add("zhujiangtao");
        list.add("pig");
        list.add("ZHU");

        for (String val : list) {
            if (val.equals("zhujiangtao")) {
                list.remove(val);
            }
        }
    }
}

运行结果是:
Java集合为什么不能在foreach中add/remove元素--全网最详细版_第1张图片
为什么会如此呢?Java 中 foreach 具体是怎么实现的呢?
我们可以先将 .java 文件编译成.class文件,然后接着 jad工具反编译 .class文件为java 文件,具体步骤如下:

  1. 在 TestFastFail.java 文件所在的目录 打开cmd 命令窗口,执行命令:javac TestFastFail.java 生成 TestFastFail.class 文件

  2. 下载 jad 工具 (jad下载地址),解压到电脑的任意位置,不需要安装,解压后包含下面2个文件:
    在这里插入图片描述

  3. 将之前生成的 TestFastFail.class 文件拷贝到 jad 解压后的文件夹内,执行如下命令:jad -s java TestFastFail.class 以生成 TestFastFail.java 文件。

反编译后的java文件如下:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   TestFastFail.java

package daily.yiyuan.com.test_java;

import java.util.*;

public class TestFastFail
{

    public TestFastFail()
    {
    }

    public static void main(String args[])
    {
        testListRemove();
    }

    private static void testListRemove()
    {
        ArrayList arraylist = new ArrayList();
        arraylist.add("zhuiangtao");
        arraylist.add("zhujiangtao");
        arraylist.add("pig");
        arraylist.add("ZHU");
        Iterator iterator = arraylist.iterator();
        do
        {
            if(!iterator.hasNext())
                break;
            String s = (String)iterator.next();//这里抛出的异常
            if(s.equals("zhujiangtao"))
                arraylist.remove(s);
        } while(true);
    }
}

可以看出来,foreach 其实是通过 do … while 和 Iterator 来实现的,但 删除 仍然调用的是 集合remove ,结果就是大家所看到的抛出了 ConcurrentModificationException异常。

为什么会抛出 ConcurrentModificationException ? 这个其实就是java的 fast-fail 机制,ArrayList 中 迭代器的 next 方法 会检查 ArrayList 中的 modCount 和 迭代器的 expectedModCount 是否相等,不相等就抛出异常。我们通过源码来分析下:

首先 我往链表里面 添加了 4 条数据,add 方法如下:

    public boolean add(E var1) {
        //扩容操作且 modCount +1
        this.ensureCapacityInternal(this.size + 1);
        this.elementData[this.size++] = var1;
        return true;
    }

ensureCapacityInternal 方法 如下:

private void ensureCapacityInternal(int var1) {
//  calculateCapacity  计算 list 的大小  this.ensureExplicitCapacity(calculateCapacity(this.elementData, var1));
    }

    private void ensureExplicitCapacity(int var1) {
        ++this.modCount; // 加1 操作
        //如果数组满了就扩容,这些很简单且不是本文的重点就不深入了
        if (var1 - this.elementData.length > 0) {
            this.grow(var1);
        }
    }

从上面的代码可知, 没调用一次 add 那么 ArrayList 的 modCount 就会加1。

remove 的方法如下:

public boolean remove(Object var1) {
        int var2;
        //如果 要删除的元素 是 null,因为 ArrayList 可以add null元素
        if (var1 == null) {
            for(var2 = 0; var2 < this.size; ++var2) {
            //找到null元素且执行删除操作,真正的删除操作fastRemove
                if (this.elementData[var2] == null) {
                    this.fastRemove(var2);
                    return true;
                }
            }
        } else {//和上面是一样的,找到待删除元素后 执行删除操作
            for(var2 = 0; var2 < this.size; ++var2) {
                if (var1.equals(this.elementData[var2])) {
                    this.fastRemove(var2);
                    return true;
                }
            }
        }

        return false;
    }

看看 真正执行的删除操作 fastRemove

 private void fastRemove(int var1) {
        ++this.modCount; // modCount 加1
        int var2 = this.size - var1 - 1;
        if (var2 > 0) {
           //通过数组的复制来删除指定元素
            System.arraycopy(this.elementData, var1 + 1, this.elementData, var1, var2);
        }
//数组最后一个位置置为null,方便GC回收
        this.elementData[--this.size] = null;
    }

上面的 remove 方法其他的我们不必关心,只要知道 remove 也会让 modCount 加1。

迭代器的 next 方法如下:

 public E next() {
 //首先会检查 modCount 和 迭代器 的属性 expectedModCount 是否相等,不相等就抛出异常
            this.checkForComodification();
            int var1 = this.cursor;
            if (var1 >= ArrayList.this.size) {
                throw new NoSuchElementException();
            } else {
                Object[] var2 = ArrayList.this.elementData;
                if (var1 >= var2.length) {
                    throw new ConcurrentModificationException();
                } else {
                    this.cursor = var1 + 1;
                    return var2[this.lastRet = var1];
                }
            }
        }

checkForComodification 代码如下:

  if (ArrayList.this.modCount != this.expectedModCount) {
                throw new ConcurrentModificationException();
            }

这个很简单就不说了。主要看看 expectedModCount 在哪里赋值的呢? 是 list.iterator()方法赋值的,代码如下:

public Iterator<E> iterator() {
        return new ArrayList.Itr();
    }

Itr 是 ArrayList 的内部类,刚才的 next 方法就是此类实现的。Itr 中的属性不多,且只有一个构造函数,如下:

        int cursor;
        int lastRet = -1;
        int expectedModCount; // 就是比较这个值和 ArrayList 的modCount 是否相等 

        Itr() {
        //将ArrayList 的 modCount 赋值给 expectedModCount 
            this.expectedModCount = ArrayList.this.modCount;
        }

通过以上的代码 我们就很清晰的知道 为什么 不能在 foreach 中执行集合的 add/remove 方法了。

为了便于分析,我们将反编译后的代码再复制一份到这里。

public class TestFastFail
{

    public TestFastFail()
    {
    }

    public static void main(String args[])
    {
        testListRemove();
    }

    private static void testListRemove()
    {
        ArrayList arraylist = new ArrayList();
        arraylist.add("zhuiangtao");
        arraylist.add("zhujiangtao");
        arraylist.add("pig");
        arraylist.add("ZHU");
        Iterator iterator = arraylist.iterator();
        do
        {
            if(!iterator.hasNext())
                break;
            String s = (String)iterator.next();
            if(s.equals("zhujiangtao"))
                arraylist.remove(s);
        } while(true);
    }
}

首先 执行了 4 次add ,那么 ArrayList 的 modCount 等于 4,

其次:Iterator iterator = arraylist.iterator(); 调用 Ite 的构造函数,把 modCount 赋值给 expectedModCount

然后:调用 next 获取集合的第一个值即“zhujiangtao”,进行比较,发现相等 那么执行集合的 remove 方法,由上面代码分析可知,执行 集合的 remove 方法后 modCount 会加1 即 modCount = 5了。

然后再一次循环执行next方法, 发现 modCount = 5expectedModCount = 4 不相等了,所以抛出了ConcurrentModificationException

那么,正确删除集合的特定元素的姿势是什么呢?其实就是调用迭代器 的 remove()方法,如下:

List<String> list = new ArrayList<>();
        list.add("zhuiangtao");
        list.add("zhujiangtao");
        list.add("pig");
        list.add("ZHU");

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()){
            String val = iterator.next();
            if (val.equals("zhujiangtao")){
                iterator.remove();
            }
        }

我们来看看 Iterator 的 remove() 方法,如下:

public void remove() {
// remove 是删除 next() 返回的元素,如果没有执行 next 那么 lastRet = -1,所以抛出异常,
            if (this.lastRet < 0) {
                throw new IllegalStateException();
            } else {
            //检查modCount 和 expectedModCount是否相等
                this.checkForComodification();

                try {
                //调用  ArrayList.this.remove(int var1)方法,这个方法会使得 modCount + 1,
                    ArrayList.this.remove(this.lastRet);
                    this.cursor = this.lastRet;
                    this.lastRet = -1;
                    // expectedModCount 重新赋值 为 modCount, 所以下次循环调用next()方法时,不会抛出异常
                    this.expectedModCount = ArrayList.this.modCount;
                } catch (IndexOutOfBoundsException var2) {
                    throw new ConcurrentModificationException();
                }
            }
        }

上面的注释已经解释的很清楚了,有不清楚的地方可以一起讨论下。

你可能感兴趣的:(集合源码分析)