Android | Java 基础 为什么在遍历的时候List不能remove,会报错ConcurrentModificationException

今天在群里聊天时(摸鱼)看见一个问题,为什么遍历List的时候不能remove?
啥?你在逗我吗?凭什么不能remove,我给你remove一个看看。

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        for (int i = 0; i < list.size(); i++) {
            list.remove(i);
        }

run!

Process finished with exit code 0

"for each遍历"
"..."

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        for (String s: list){
            list.remove(s);
        }
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at practice.ListTest.main(ListTest.java:14)

遇事不决看源码

原因

众所周知,for each的本质就是Iterator在next()查询元素,将java文件编译后的class文件打开即可看到

        List<String> list = new ArrayList();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        Iterator var2 = list.iterator();

        while(var2.hasNext()) {
            String s = (String)var2.next();
            list.remove(s);
        }

然后查看ArrayList.java:851源码,ArrayList的Iterator 在next()最开始之前进行检查,同样的remove方法也会进行checkForComodification()检查。

  public E next() {
            checkForComodification();
            ...
        }

  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();
            }
        }

然后打开此方法,当modCount 不等于expectedModCount的时候就会抛出该异常,那么这个modCount 和expectedModCount又是什么呢?

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

先看arraylist的add方法

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

然后点进去

  private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

  private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

这里的方法是判断arraylist在添加元素的时候是否需要扩容,在ensureExplicitCapacity方法里找到了modCount++,也就是每次添加元素时就会增加。接下来看expectedModCount 。

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;
        ...
}

我们看到,看arralist的Iterator遍历器的实例变量里,expectedModCount 就等于modCount,也就是说在一开始expectedModCount的数量等于arralist的数量,这也就说明了,在第一次Iterator的next方法里并没有报错,因为modCount = expectedModCount,所以错误只能出在第二次next方法里,然后接下来看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;
    }

  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
    }

看到fastRemove方法里的第一行应该就清楚原因了,remove的时候modCount增加了,和一开始的expectedModCount ,也就是arraylist的一开始的数量不一致了,所以会导致ConcurrentModificationException。
所以正确的用法是什么?

正确用法

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

why?

      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();
            }
        }

延伸

那么有没有一种list,能直接就在遍历的时候直接进行删除呢?
答案肯定是有的(听大佬说的)
CopyOnWriteArrayList

不信?试试

        List<String> list = new CopyOnWriteArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        for(String s : list){
           list.remove(s);
        }

Process finished with exit code 0

CopyOnWriteArrayList如何做到的?
CopyOnWriteArrayList 类的所有可变操作(add,set等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,并不直接修改原有数组对象,而是对原有数据进行一次拷贝,将修改的内容写入副本中。写完之后,再将修改完的副本替换成原来的数据,这样就可以保证写操作不会影响读操作了。

从 CopyOnWriteArrayList 的名字可以看出,CopyOnWriteArrayList 是满足 CopyOnWrite 的 ArrayList,所谓 CopyOnWrite 的意思:、就是对一块内存进行修改时,不直接在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后,再将原来指向的内存指针指到新的内存,原来的内存就可以被回收。

看看它的add方法

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();//获取当前已有的数组
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝一份新的数组
            newElements[len] = e;//将增加的值放入新的数组
            setArray(newElements);//把当前数组对象设置为刚刚拷贝的数组值
            return true;
        } finally {
            lock.unlock();
        }
    }

remove(object)方法

  public boolean remove(Object o) {
        Object[] snapshot = getArray();
        int index = indexOf(o, snapshot, 0, snapshot.length);//找到当前元素的下标索引值
        return (index < 0) ? false : remove(o, snapshot, index);
    }

    private boolean remove(Object o, Object[] snapshot, int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();//加锁
        try {
            Object[] current = getArray();//再次获取当前数组
            int len = current.length;
            if (snapshot != current) findIndex: {//查找需要移除的元素在数组里的索引
                int prefix = Math.min(index, len);
                for (int i = 0; i < prefix; i++) {
                    if (current[i] != snapshot[i] && eq(o, current[i])) {
                        index = i;
                        break findIndex;
                    }
                }
                if (index >= len)
                    return false;
                if (current[index] == o)
                    break findIndex;
                index = indexOf(o, current, index, len);
                if (index < 0)
                    return false;
            }
            Object[] newElements = new Object[len - 1];//创建一个新的数组,拷贝
            System.arraycopy(current, 0, newElements, 0, index);
            System.arraycopy(current, index + 1,
                             newElements, index,
                             len - index - 1);
            setArray(newElements);//设置拷贝后的数组
            return true;
        } finally {
            lock.unlock();//释放锁
        }
    }

从add方法和remove方法里不难看出,不管是添加元素还是移除元素,都是通过拷贝数组并重新赋值来实现的,所以在遍历时,remove或者add或者其他一些列操作都不会引起和arraylist一样的异常的,甚至,你在使用Iterator的时候,它还会报错。

private static class COWSubListIterator<E> implements ListIterator<E> {
        ...
        public void remove() {
            throw new UnsupportedOperationException();
        }

        public void set(E e) {
            throw new UnsupportedOperationException();
        }

        public void add(E e) {
            throw new UnsupportedOperationException();
        }
         ...
}


List<String> list = new CopyOnWriteArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        Iterator var2 = list.iterator();

        while(var2.hasNext()) {
            String s = (String)var2.next();
            var2.remove();
        }


Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1176)
    at practice.ListTest.main(ListTest.java:19)

总结

1.ArrayList在foreach的时候不能直接使用list.remove来操作数组,因为ArrayList的Iterator 的next方法里每次都会判断当前的数组的数量是否和修改后的数量是否对等,也就是expectedModCount 和modCount,而list.remove方法会修modCount的数量,所以下一次判断时就不对等,就会报错。
2.CopyOnWriteArrayList 可以实现遍历时直接list.remove,因为CopyOnWriteArrayList 的增删是通过每次都拷贝一次数组重新赋值实现的。
3.这个算是java的基础,我居然都不知道。
4.我是不可回收垃圾。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容