zoukankan      html  css  js  c++  java
  • ArrayList在for循环中remove所产生的问题

    背景:

    刚入职公司的时候,就听到面试官在面试过程中提问ArrayList在for循环中remove的问题,当时很庆幸自己没被问到,一年后又一次听到面试在问这个问题。发现自己还没有深入研究一下,所以就有了今天这篇文章。

    代码如下:

    import java.util.ArrayList;
    import java.util.List;
    
    public class ArrayListTest {
        public static void main(String[] args) {
            List<String> list = new ArrayList<>();
            list.add("aaa");
            list.add("bbb");
            list.add("ccc");
            list.add("ddd");
    
            for (String str : list) {
                if ("aaa".equals(str)) {
                    list.remove("aaa");
                }
            }
        }
    }
    

    以上代码的执行会导致以下异常:

    Exception in thread "main" java.util.ConcurrentModificationException
    	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
    	at java.util.ArrayList$Itr.next(ArrayList.java:859)
    	at future.ArrayListTest.main(ArrayListTest.java:14)
    

    今天通过一种通俗易懂的方式说明代码异常的原因!

    1️⃣list.add

    通过查看 list.add 方法,发现有一个变量的值发生了改变,这个变量叫 modCount 。在上面demo中一共执行了四次 add 操作,所以 modCount 的值为4.

    注:list 的 add()、remove() 和 clear() 都会改变 modCount 值。

    image-20210421143503040

    2️⃣for(String str : list)

    for(String str : list) 调用的是 ArrayList 的内部类 Itr ,Itr是对 Iterator 的实现。在 Iterator 开始之前,会先执行 modCount != expectedModCount 。

    此时 modCount 和 expectedModCount 的值均为 4 .

    3️⃣list.remove("aaa")

    先看一下报错原因,这里是源码:

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

    即 modcount 和 expectedModCount 的值不相等了。看到这里应该有人会想,如果每次执行 remove 后,将 expectedModCount = modcount 不就好了。其实是有的,只是我们调用的方法错了。

    list.remove("aaa")调用的 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])) {
                    //走的是这个remove
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    

    而使 modCount 的值改变的是其中的 fastRemove 方法。

    fastRemove 源码如下:

    /*
         * Private remove method that skips bounds checking and does not
         * return the value removed.
         */
    private void fastRemove(int index) {
        //此处 modCount + 1
        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(E e) 方法执行完成后只执行了 modcount++,并没有将值赋给 expectedModCount。

    而真正的 expectedModCount = modCount 执行的源码如下:

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

    该方法位于内部类 Itr 中。

    这也就是说为什么如果 list 在循环中有删除操作,最好用 Iterator 的方式来做。

    结论

    简单总结一下

    • list.remove() 没有对 expectedModCount 重新赋值
    • iterator.remove() 每次 remove 之后会对 expectedModCount 重新赋值
    If you’re going to reuse code, you need to understand that code!
  • 相关阅读:
    hdu5360 Hiking(水题)
    hdu5348 MZL's endless loop(欧拉回路)
    hdu5351 MZL's Border(规律题,java)
    hdu5347 MZL's chemistry(打表)
    hdu5344 MZL's xor(水题)
    hdu5338 ZZX and Permutations(贪心、线段树)
    hdu 5325 Crazy Bobo (树形dp)
    hdu5323 Solve this interesting problem(爆搜)
    hdu5322 Hope(dp)
    Lightoj1009 Back to Underworld(带权并查集)
  • 原文地址:https://www.cnblogs.com/leizzige/p/14685275.html
Copyright © 2011-2022 走看看