關於集合遍歷並刪除報錯詳解

Algoric發表於2019-03-12

一、list集合正確刪除

list集合刪除不要使用增強for,建議使用for(int i;;)這種方法,注意這種方法刪除集合元素會導致索引前移導致遍歷問題

例如:

private static void delFor() {
        List<String> blist = new ArrayList<>();
        blist.add("a");
        blist.add("b");
        blist.add("c");
        blist.add("d");
        blist.add("e");
        blist.add("f");


        for (int i = 0; i < blist.size(); i++) {
            if(blist.get(i).equals("b")) {
                blist.remove(blist.get(i));
                //這裡輸出被刪除的元素有問題,主要是索引改變,刪除本身沒問題
                System.out.println("刪除的元素是: " +  blist.get(i));
            }

        }
        System.out.println(blist);
    }
複製程式碼

二、foreach刪除

增強for遍歷等效於使用迭代器, 在遍歷中修改元素數目會報ConcurrentModificationException

private static void delNormal() {
        List<String> alist = new ArrayList<>();
        alist.add("1");
        alist.add("2");
        alist.add("3");

        for (String item : alist) {
            if (item.equals("2")) {  //刪除2不報錯
                alist.remove(item);
                System.out.println("被刪除的元素"+item);
            }

        }
        System.out.println("遍歷刪除後的集合" + "  " + alist);
    }
    
    結果:被刪除的元素2
          遍歷刪除後的集合  [1, 3]
複製程式碼

上面結論大家肯定知道,但是看這段程式碼,刪除成功了,並沒有報錯,結論不對嗎?

再看一段程式碼:

private static void del() {
        List<String> blist = new ArrayList<>();
        blist.add("a");
        blist.add("b");
        blist.add("c");
        blist.add("d");
        blist.add("e");
        blist.add("f");
        for (String item : blist) {
            if(item.equals("b")) {
                blist.remove(item);
                System.out.println("刪除的元素是: "+ item );
            }
        }

        System.out.println("刪除後的集合為:" +blist);
    }
複製程式碼

這段程式碼執行直接報錯,是上面描述的異常

private static void delForIterator() {
        List<String> blist = new ArrayList<>();
        blist.add("a");
        blist.add("b");
        blist.add("c");
        blist.add("d");
        blist.add("e");
        blist.add("f");
        Iterator it = blist.iterator();
        while(it.hasNext()) {
            String item = (String) it.next();
            if(item.equals("b")) {
                it.remove();
                System.out.println("刪除的元素是: "+ item );
            }

        }

        System.out.println("刪除後的集合為:" +blist);
    }
複製程式碼

正確刪除,沒問題

我們看下迭代刪除的原始碼:

private class Itr implements Iterator<E> {
        int cursor;       // /將要訪問的元素的索引
        int lastRet = -1; // 上一個訪問元素的索引
        int expectedModCount = modCount;//expectedModCount為預期修改值,初始化等於modCount(AbstractList類中的一個成員變數)

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            //每次呼叫next()需要check
            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值相等
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }
複製程式碼

在獲取一個Iterator物件時,會初始化成員變數

  • cursor(0)
  • lastRet(-1)
  • expectedModCount(ModCount=初始集合長度).

對於第一段程式碼(增強for底層還是呼叫迭代器),不報錯是因為在刪除2以後,呼叫hasNext()方法,cursor值移動至2,size此時變成2,相等,跳出迴圈,所以沒有報錯,這僅僅是個巧合而已。

增加for刪除報錯的主要原因是每次呼叫next()方法,都會檢查expectedModCount和 ModCount值是否相等,當我們刪除元素後,ModCount會改變與expectedModCount值不同,引起報錯。

使用iterator刪除時,看上面原始碼,會再次賦值它們相等,所以不會報錯。

三、多執行緒情況下的集合刪除

使用迭代器的iterator.remove()在單執行緒下是不會報錯的,但是在多執行緒情況下,一個執行緒修改了集合的modCount導致另外一個執行緒迭代時modCount與該迭代器的expectedModCount不相等,這也會報異常。

public class RemoveListForThreads implements Runnable {
    static List<String> alist = new ArrayList<>();

    public static void main(String[] args) {
        RemoveListForThreads s = new RemoveListForThreads();
        alist.add("a");
        alist.add("b");
        alist.add("c");
        alist.add("d");

        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 1,
                TimeUnit.SECONDS, workQueue);

        for (int i = 0; i < 5; i++) {
            executor.execute(s);
        }
        executor.shutdown();
    }


    @Override
    public synchronized void run() {
        Iterator<String> iterator = alist.iterator();
            while (iterator.hasNext()) {
                String s = iterator.next();
                if (s.equals("c") && Thread.currentThread().getName().equals("pool-1-thread-1")) {
                    iterator.remove();
                    System.out.println(Thread.currentThread().getName() + " " + s);
                }
                System.out.println(Thread.currentThread().getName() + " " + s);
            }
            System.out.println(alist);
        }
}

複製程式碼

上面這段程式碼建立了一個執行緒池,開啟了五個執行緒,在run方法中遍歷並刪除“b”元素,如果該方法不加同步鎖sychronized,也會丟擲異常

相關文章