對JAVA集合進行遍歷刪除時務必要用迭代器


今天同事寫了幾行類似這樣的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
public  static  void  main(String args[]) {
     List<String> famous =  new  ArrayList<String>();
     famous.add( "liudehua" );
     famous.add( "madehua" );
     famous.add( "liushishi" );
     famous.add( "tangwei" );
     for  (String s : famous) {
         if  (s.equals( "madehua" )) {
             famous.remove(s);
         }
     }
}

運行出異常:

Exception in thread "main" java.util.ConcurrentModificationException

at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)

at java.util.AbstractList$Itr.next(AbstractList.java:343)

at com.bes.Test.main(Test.java:15)

Java新手最容易犯的錯誤,對JAVA集合進行遍歷刪除時務必要用迭代器。切記。

其實對於如上for循環,運行過程中還是轉換成了如下代碼:

1
2
3
4
5
6
for (Iterator<String> it = famous.iterator();it.hasNext();){
          String s = it.next();
          if (s.equals( "madehua" )){
              famous.remove(s);
          }
      }

仍然采用的是迭代器,但刪除操作卻用了錯誤的方法。如將famous.remove(s)改成it.remove()

則運行正常,結果也無誤。

當然如果改成:

1
2
3
4
5
6
for  ( int  i =  0 ; i < famous.size(); i++) {
             String s = famous.get(i);
             if  (s.equals( "madehua" )) {
                 famous.remove(s);
             }
         }

這種方法,也是可以完成功能,但一般也不這么寫。

為什么用了迭代碼器就不能采用famous.remove(s)操作? 這種因為ArrayList與Iterator混合使用時會導致各自的狀態出現不一樣,最終出現異常。

我們看一下ArrayList中的Iterator實現:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private  class  Itr  implements  Iterator<E> {
    /**
     * Index of element to be returned by subsequent call to next.
     */
    int  cursor =  0 ;
    /**
     * Index of element returned by most recent call to next or
     * previous.  Reset to -1 if this element is deleted by a call
     * to remove.
     */
    int  lastRet = - 1 ;
    /**
     * The modCount value that the iterator believes that the backing
     * List should have.  If this expectation is violated, the iterator
     * has detected concurrent modification.
     */
    int  expectedModCount = modCount;
    public  boolean  hasNext() {
            return  cursor != size();
    }
    public  E next() {
            checkForComodification();
        try  {
        E next = get(cursor);
        lastRet = cursor++;
        return  next;
        catch  (IndexOutOfBoundsException e) {
        checkForComodification();
        throw  new  NoSuchElementException();
        }
    }
    public  void  remove() {
        if  (lastRet == - 1 )
        throw  new  IllegalStateException();
            checkForComodification();
        try  {
        AbstractList. this .remove(lastRet);
        if  (lastRet < cursor)
            cursor--;
        lastRet = - 1 ;
        expectedModCount = modCount;
        catch  (IndexOutOfBoundsException e) {
        throw  new  ConcurrentModificationException();
        }
    }
    final  void  checkForComodification() {
        if  (modCount != expectedModCount)
        throw  new  ConcurrentModificationException();
    }
    }

基本上ArrayList采用size屬性來維護自已的狀態,而Iterator采用cursor來來維護自已的狀態。

當size出現變化時,cursor並不一定能夠得到同步,除非這種變化是Iterator主動導致的。

從上面的代碼可以看到當Iterator.remove方法導致ArrayList列表發生變化時,他會更新cursor來同步這一變化。但其他方式導致的ArrayList變化,Iterator是無法感知的。ArrayList自然也不會主動通知Iterator們,那將是一個繁重的工作。Iterator到底還是做了努力:為了防止狀態不一致可能引發的無法設想的后果,Iterator會經常做checkForComodification檢查,以防有變。如果有變,則以異常拋出,所以就出現了上面的異常。

 

 如果對正在被迭代的集合進行結構上的改變(即對該集合使用add、remove或clear方法),那么迭代器就不再合法(並且在其后使用該迭代器將會有ConcurrentModificationException異常被拋出).

如果使用迭代器自己的remove方法,那么這個迭代器就仍然是合法的。

復制代碼
package chapter1;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * Created by MyWorld on 2016/3/3.
 */
public class FastFailResolver {

    public static void main(String[] args) {
        Map<String, String> source = new HashMap<String, String>();
        for (int i = 0; i < 10; i++) {
            source.put("key" + i, "value" + i);
        }
        System.out.println("Source:" + source);
//        fastFailSceneWhenRemove(source);
        commonSceneWhenRemove(source);

    }

    private static void commonSceneWhenRemove(Map<String, String> source) {
        Iterator<Map.Entry<String, String>> iterator = source.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, String> entry = iterator.next();
            if (entry.getKey().contains("1")) {
                iterator.remove();
            }
        }
        System.out.println(source);
    }

    private static void fastFailSceneWhenRemove(Map<String, String> source) {
        for (Map.Entry<String, String> entry : source.entrySet()) {
            if (entry.getKey().contains("1")) {
                source.remove(entry.getKey());
            }
        }
        System.out.println(source);
    }


}
復制代碼

 

 

 

 

 

3.在一個循環中刪除一個列表中的元素

思考下面這一段在循環中刪除多個元素的的代碼

1
2
3
4
5
ArrayList<String> list = new ArrayList<String>(Arrays.asList( "a" , "b" , "c" , "d" ));
for ( int i= 0 ;i<list.size();i++){
     list.remove(i);
}
System.out.println(list);

輸出結果是:

1
[b,d]

在這個方法中有一個嚴重的錯誤。當一個元素被刪除時,列表的大小縮小並且下標變化,所以當你想要在一個循環中用下標刪除多個元素的時候,它並不會正常的生效。

與下面結合的一個示例:

復制代碼
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>(Arrays.asList("a","a", "b",
                "c", "d"));
        for (int i = 0; i < list.size(); i++) {
            if (list.get(i).equals("a")) {
                list.remove(i);
            }
        }
        System.out.println(list);
    }
復制代碼

輸出:

[a, b, c, d]

即輸出與預期不一致


你也許知道在循環中正確的刪除多個元素的方法是使用迭代,並且你知道java中的foreach循環看起來像一個迭代器,但實際上並不是。考慮一下下面的代碼:

1
2
3
4
5
6
ArrayList<String> list = new ArrayList<String>(Arrays.asList( "a" , "b" , "c" , "d" ));
for (String s:list){
     if (s.equals( "a" )){
         list.remove(s);
     }
}

它會拋出一個ConcurrentModificationException異常。 相反下面的顯示正常:

1
2
3
4
5
6
7
8
ArrayList<String> list = new ArrayList<String>(Arrays.asList( "a" , "b" , "c" , "d" ));
Iterator<String> iter = list.iterator();
while (iter.hasNext()){
         String s = iter.next();
         if (s.equals( "a" )){
             iter.remove();
     }
}

.next()必須在.remove()之前調用。在一個foreach循環中,編譯器會使.next()在刪除元素之后被調用,因此就會拋出ConcurrentModificationException異常,你也許希望看一下ArrayList.iterator()的源代碼。

http://www.cnblogs.com/softidea/p/4279574.html

 

復制代碼

import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class IteratorTest{ public static void main(String[] args) { List<String> list = new ArrayList<String>(); list.add("Test1"); list.add("Test2"); list.add("Test3"); list.add("Test4"); list.add("Test5"); for(Iterator<String> it = list.iterator();it.hasNext();){ if(it.next().equals("Test3")){ it.remove(); } } for(String s : list){ System.out.println(s); } } }
復制代碼

Iterator 支持從源集合中安全地刪除對象,只需在 Iterator 上調用 remove() 即可。這樣做的好處是可以避免 ConcurrentModifiedException ,這個異常顧名思意:當打開 Iterator 迭代集合時,同時又在對集合進行修改。
有些集合不允許在迭代時刪除或添加元素,但是調用 Iterator 的remove() 方法是個安全的做法。

 

java.util.ConcurrentModificationException詳解

http://blog.csdn.net/smcwwh/article/details/7036663

 

【引言】

經常在迭代集合元素時,會想對集合做修改(add/remove)操作,類似下面這段代碼:

復制代碼
for (Iterator<Integer> it = list.iterator(); it.hasNext(); ) {
    Integer val = it.next();
    if (val == 5) {
        list.remove(val);
    }
}
復制代碼

運行這段代碼,會拋出異常java.util.ConcurrentModificationException。

【解惑】

(以ArrayList來講解)在ArrayList中,它的修改操作(add/remove)都會對modCount這個字段+1,modCount可以看作一個版本號,每次集合中的元素被修改后,都會+1(即使溢出)。接下來再看看AbsrtactList中iteraor方法

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

 

它返回一個內部類,這個類實現了iterator接口,代碼如下:

復制代碼
private class Itr implements Iterator<E> {
    int cursor = 0;

    int lastRet = -1;

    int expectedModCount = modCount;

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

    public E next() {
        checkForComodification();
        try {
            E next = get(cursor);
            lastRet = cursor++;
            return next;
        } catch (IndexOutOfBoundsException e) {
            checkForComodification();
            throw new NoSuchElementException();
        }
    }

    public void remove() {
        if (lastRet == -1)
            throw new IllegalStateException();
        checkForComodification();

        try {
            AbstractList.this.remove(lastRet);
            if (lastRet < cursor)
                cursor--;
            lastRet = -1;
            // 修改expectedModCount 的值
            expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
            throw new ConcurrentModificationException();
        }
    }

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

在內部類Itr中,有一個字段expectedModCount ,初始化時等於modCount,即當我們調用list.iterator()返回迭代器時,該字段被初始化為等於modCount。在類Itr中next/remove方法都有調用checkForComodification()方法,在該方法中檢測modCount == expectedModCount,如果不相當則拋出異常ConcurrentModificationException。

前面說過,在集合的修改操作(add/remove)中,都對modCount進行了+1。
在看看剛開始提出的那段代碼,在迭代過程中,執行list.remove(val),使得modCount+1,當下一次循環時,執行 it.next(),checkForComodification方法發現modCount != expectedModCount,則拋出異常。

【解決辦法】
如果想要在迭代的過程中,執行刪除元素操作怎么辦?
再來看看內部類Itr的remove()方法,在刪除元素后,有這么一句expectedModCount = modCount,同步修改expectedModCount 的值。所以,如果需要在使用迭代器迭代時,刪除元素,可以使用迭代器提供的remove方法。對於add操作,則在整個迭代器迭代過程中是不允許的。 其他集合(Map/Set)使用迭代器迭代也是一樣。

 

 當使用 fail-fast iterator 對 Collection 或 Map 進行迭代操作過程中嘗試直接修改 Collection / Map 的內容時,即使是在單線程下運行,  java.util.ConcurrentModificationException 異常也將被拋出。   
Iterator 是工作在一個獨立的線程中,並且擁有一個 mutex 鎖。 
Iterator 被創建之后會建立一個指向原來對象的單鏈索引表,當原來的對象數量發生變化時,這個索引表的內容不會同步改變,所以當索引指針往后移動的時候就找不到要迭代的對象,所以按照 fail-fast 原則 Iterator 會馬上拋出 java.util.ConcurrentModificationException 異常。

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

 

有意思的是如果你的 Collection / Map 對象實際只有一個元素的時候, ConcurrentModificationException 異常並不會被拋出。這也就是為什么在 javadoc 里面指出: it would be wrong to write a program that depended on this exception for its correctness: ConcurrentModificationException should be used only to detect bugs.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM