溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何在java集合類遍歷的同時(shí)進(jìn)行刪除操作

發(fā)布時(shí)間:2021-09-14 11:19:41 來源:億速云 閱讀:110 作者:柒染 欄目:開發(fā)技術(shù)

如何在java集合類遍歷的同時(shí)進(jìn)行刪除操作,針對(duì)這個(gè)問題,這篇文章詳細(xì)介紹了相對(duì)應(yīng)的分析和解答,希望可以幫助更多想解決這個(gè)問題的小伙伴找到更簡(jiǎn)單易行的方法。

java集合類遍歷的同時(shí)進(jìn)行刪除操作

1. 背景

在使用java的集合類遍歷數(shù)據(jù)的時(shí)候,在某些情況下可能需要對(duì)某些數(shù)據(jù)進(jìn)行刪除。往往操作不當(dāng),便會(huì)拋出一個(gè)ConcurrentModificationException,本方簡(jiǎn)單說明一下錯(cuò)誤的示例,以及一些正確的操作并簡(jiǎn)單的分析下原因。

P.S. 示例代碼和分析是針對(duì)List的實(shí)例類ArrayList,其它集合類可以作個(gè)參考。

2. 代碼示例

示例代碼如下,可以根據(jù)注釋來說明哪種操作是正確的:

public class TestIterator {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");
        print(list);
 
        // 操作1:錯(cuò)誤示范,不觸發(fā)ConcurrentModificationException
        System.out.println("NO.1");
        List<String> list1 = new ArrayList<>(list);
        for(String str:list1) {
            if ("4".equals(str)) {
                list1.remove(str);
            }
        }
        print(list1);
        // 操作2:錯(cuò)誤示范,使用for each觸發(fā)ConcurrentModificationException
        System.out.println("NO.2");
        try{
            List<String> list2 = new ArrayList<>(list);
            for(String str:list2) {
                if ("2".equals(str)) {
                    list2.remove(str);
                }
            }
            print(list1);
        }catch (Exception e) {
            e.printStackTrace();
        }
        // 操作3:錯(cuò)誤示范,使用iterator觸發(fā)ConcurrentModificationException
        try{
            System.out.println("NO.3");
            List<String> list3 = new ArrayList<>();
            Iterator<String> iterator3 = list3.iterator();
            while (iterator3.hasNext()) {
                String str = iterator3.next();
                if ("2".equals(str)) {
                    list3.remove(str);
                }
            }
            print(list3);
        }catch (Exception e){
            e.printStackTrace();
        }
        // 操作4: 正確操作
        System.out.println("NO.4");
        List<String> list4 = new ArrayList<>(list);
        for(int i = 0; i < list4.size(); i++) {
            if ("2".equals(list4.get(i))) {
                list4.remove(i);
                i--; // 應(yīng)當(dāng)有此操作
            }
        }
        print(list4);
        // 操作5: 正確操作
        System.out.println("NO.5");
        List<String> list5 = new ArrayList<>(list);
        Iterator<String> iterator = list5.iterator();
        while (iterator.hasNext()) {
            String str = iterator.next();
            if ("2".equals(str)) {
                iterator.remove();
            }
        }
        print(list5);
 
    }
 
    public static void print(List<String> list) {
        for (String str : list) {
            System.out.println(str);
        }
    }
}

P.S. 上面的示例代碼中,操作1、2、3都是不正確的操作,在遍歷的同時(shí)進(jìn)行刪除,操作4、5能達(dá)到預(yù)期效果,推建使用第5種寫法。

3. 分析

首先,需要先聲明3個(gè)東東:

  • 1. for each底層采用的也是迭代器的方式(這個(gè)我并沒有驗(yàn)證,是查找相關(guān)資料得知的),所以對(duì)for each的操作,我們只需要關(guān)注迭代器方式的實(shí)現(xiàn)即可。

  • 2. AraayList底層是采用數(shù)組進(jìn)行存儲(chǔ)的,所以操作4實(shí)現(xiàn)是不同于其它(1、2、3、5)操作的,他們用的都是迭代器方式。

  • 3. 鑒于1、2點(diǎn),其實(shí)本文重點(diǎn)關(guān)注的是采用迭代器的remove(操作5)為什么沒有問題,而采用集合的remove(操作1、2、3)就不行。

3.1 為什么操作4沒有問題

// 操作4: 正確操作
        System.out.println("NO.4");
        List<String> list4 = new ArrayList<>(list);
        for(int i = 0; i < list4.size(); i++) {
            if ("2".equals(list4.get(i))) {
                list4.remove(i);
                i--; // 應(yīng)當(dāng)有此操作
            }
        }

這個(gè)其實(shí)沒什么太多說的,ArrayList底層采用數(shù)組,它刪除某個(gè)位置的數(shù)據(jù)實(shí)際上就是把從這個(gè)位置下一位開始到最后位置的數(shù)據(jù)在數(shù)組里整體前移一位(基本知識(shí),不多說明)。所以在遍歷的時(shí)候,重點(diǎn)其實(shí)是索引值的大小,底層實(shí)現(xiàn)是需要依賴這個(gè)索引 的,這也是為什么最后有個(gè)i--,因?yàn)槲覀儎h除2的時(shí)候,索引值i為1,刪除的時(shí)候,就把索引為2到list.size()-1的數(shù)據(jù)都前移一位,如果不把i-1,那么下一輪循環(huán)時(shí),i的值就為2,這樣就把原來索引值為2,而現(xiàn)在索引值為1的數(shù)據(jù)給漏掉了,這個(gè)地方需要注意一下。比如,如果原數(shù)據(jù)中索引1、2的數(shù)據(jù)都為2,想把2都刪除掉,如果不進(jìn)行i--,那么把索引1處的2刪除掉后,下一次循環(huán)判斷時(shí),就會(huì)把原來索引為2,現(xiàn)在索引為1的這個(gè)2給遺漏掉了。

3.2 采用迭代時(shí)ConcurrentModificationException產(chǎn)生的原因

其實(shí)這個(gè)異常是在迭代器的next()方法體調(diào)用checkForComodification()時(shí)拋出來的:

看下迭代器的這兩個(gè)方法的源碼:

   public E next() {
            checkForComodification();//注意這個(gè)方法,會(huì)在這里拋出
            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];
        }
        final void checkForComodification() {
            // 問題就在modCount與expectedModCount的值
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

1. 首先說明,modCount這個(gè)值是ArrayList的一個(gè)變量,而expectedModCount是迭代器的一個(gè)變量。

modCount:該值是在集合的結(jié)構(gòu)發(fā)生改變時(shí)(如增加、刪除等)進(jìn)行一個(gè)自增操作,其實(shí)在ArrayList中,只有刪除元素時(shí)這個(gè)值才發(fā)生改變。

expectedModCount:該值在調(diào)用集合的iterator()方法實(shí)例化迭代器的時(shí)候,會(huì)將modCount的值賦值給迭代器的變量 expectedModCount。也就是說,在該迭代器的迭代操作期間,expectedModCount的值在初始化之后便不會(huì)進(jìn)行改變,而modCount的值卻可能改變(比如進(jìn)行了刪除操作),這也是每次調(diào)用next()方法的時(shí)候,為什么要比較下這兩個(gè)值是否一致。

其實(shí),我是把它們看作類似于CAS理論的實(shí)現(xiàn)來理解的,其實(shí)在迭代器遍歷的時(shí)候調(diào)用集合的remove方法,代碼上看起來是串行的,但是可以認(rèn)為是兩個(gè)不同線程的并行操作這個(gè)ArrayList對(duì)象(我也是看了下其它資料,才試著這樣去理解)。

3.3 為什么在遍歷時(shí)使用迭代器的remove沒有問題

依據(jù)3.2條,我們知道,既然使用ArrayList的remove方法出現(xiàn)ConcurrentModificationException的原因在于modCount與expectedCount的值,那么問題就很明晰了,先看下迭代器的remove方法的源碼:

public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            // 雖然這里也調(diào)用了這方法,但是本次我們可以先忽略,因?yàn)檫@個(gè)remove()方法是
            //iterator自已的,也就是可以看作遍歷和刪除是串行發(fā)生的,目前我們尚未開始進(jìn)行移除
            //操作,所以這里的校驗(yàn)不應(yīng)該拋出異常,如果拋出了ConcurrentModificationException,
            //那只能是其它線程改了當(dāng)前集合的結(jié)構(gòu)導(dǎo)致的,并不是因?yàn)槲覀儽敬紊形撮_始的移除操作
            checkForComodification();
 
            try {
                // 這里開始進(jìn)行移除
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                // 重新賦值,使用expectedModCount與modCount的值保持一致
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
     }

注意看下我的注釋,在調(diào)用迭代器的remove方法時(shí),雖然也是在調(diào)用集合的remove方法 ,但是因?yàn)檫@里保持了modCount與expectedModCount的數(shù)據(jù)一致性,所以在下次調(diào)用next()方法,調(diào)用checkForComodification方法時(shí),也就不會(huì)拋出ConcurrentModificationException了。

3.4 為什么操作1沒有拋出ConcurrentModificationException

其實(shí)操作1雖然使用for each但是上面說過,其實(shí)底層依然是迭代器的方式,這既然是迭代器,然而采用集合的remove方法,卻沒有拋出ConcurrentModificationException, 這是因?yàn)橐瞥脑厥堑箶?shù)第二個(gè)元素的原因。

迭代器迭代的時(shí)候,調(diào)用hasNext()方法來判斷是否結(jié)束迭代,若沒有結(jié)束,才開始調(diào)用next()方法,獲取下一個(gè)元素,在調(diào)用next()方法的時(shí)候,因?yàn)檎{(diào)用 checkForComodification方法時(shí)拋出了ConcurrentModificationException。

所以,如果在調(diào)用hasNext()方法之后結(jié)束循環(huán),不調(diào)用next()方法,就不會(huì)發(fā)生后面的一系列操作了。

既然還有最后一個(gè)元素,為什么會(huì)結(jié)束循環(huán),問題就在于hasNext()方法,看下源碼:

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

其實(shí)每次調(diào)用next()方法迭代的時(shí)候,cursor都會(huì)加1,cursor就相當(dāng)于一個(gè)游標(biāo),當(dāng)它不等于集合大小size的時(shí)候,就會(huì)一直循環(huán)下去,但是因?yàn)椴僮?移除了一個(gè)元素,導(dǎo)致集合的size減一,導(dǎo)致在調(diào)用hasNext()方法,結(jié)束了循環(huán),不會(huì)遍歷最后一個(gè)元素,也就不會(huì)有后面的問題了。

java集合中的一個(gè)移除數(shù)據(jù)陷阱

遍歷集合自身并同時(shí)刪除被遍歷數(shù)據(jù)

使用Set集合時(shí):遍歷集合自身并同時(shí)刪除被遍歷到的數(shù)據(jù)發(fā)生異常

Iterator<String> it = atomSet.iterator(); 
  while (it.hasNext()) {  
   if (!vars.contains(it.next())) {
    atomSet.remove(it.next());
   }
  }

拋出異常:

Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextEntry(HashMap.java:793)
at java.util.HashMap$KeyIterator.next(HashMap.java:828)
at test2.Test1.main(Test1.java:16)

異常本質(zhì)原因

Iterator 是工作在一個(gè)獨(dú)立的線程中,并且擁有一個(gè) mutex 鎖。 Iterator 被創(chuàng)建之后會(huì)建立一個(gè)指向原來對(duì)象的單鏈索引表,當(dāng)原來的對(duì)象數(shù)量發(fā)生變化時(shí),這個(gè)索引表的內(nèi)容不會(huì)同步改變,所以當(dāng)索引指針往后移動(dòng)的時(shí)候就找不到要迭代的對(duì)象,所以按照 fail-fast 原則 Iterator 會(huì)馬上拋出 java.util.ConcurrentModificationEx ception 異常。

所以 Iterator 在工作的時(shí)候是不允許被迭代的對(duì)象被改變的。但你可以使用 Iterator 本身的方法 remove() 來刪除對(duì)象, Iterator.remove() 方法會(huì)在刪除當(dāng)前迭代對(duì)象的同時(shí)維護(hù)索引的一致性。

解決

使用Iterator的remove方法

Iterator<String> it = atomVars.iterator();  
   while (it.hasNext()) {  
    if (!vars.contains(it.next())) {
     it.remove();
    }
   }

代碼能夠正常執(zhí)行。

關(guān)于如何在java集合類遍歷的同時(shí)進(jìn)行刪除操作問題的解答就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關(guān)注億速云行業(yè)資訊頻道了解更多相關(guān)知識(shí)。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI