List.remove()的使用注意


  今天修改一個bug,需要取一個List和一個Set的交集,使用了雙重循環。想着提高循環效率,每加入一個交集中的元素,就將List中的元素刪除,減少不必要的循環。結果直接調用了List的remove()方法,拋出了java.util.ConcurrentModificationException異常。這時才忽然記起之前看過的List循環中使用remove()方法要特別注意,尤其是forEach的循環。

不使用forEach的循環

  使用常規的for循環寫法,代碼如下:


    public static void main(String [] args){
        List<String> list = new ArrayList<String>();
        list.add("111");
        list.add("111");
        list.add("111");
        list.add("333");
        list.add("333");

        for (int i = 0; i < list.size(); i++){
            if ("111".equals(list.get(i))){
                list.remove("111");
// i--;不加這句會少刪除一個111
            }
            System.out.println(list.size());
        }
    }

  先說說list的remove()方法,該方法有兩個,一個是remove(Object obj),另一個是remove(int index)。根據參數很容易理解,而這里要說的是remove(obj)會刪除list中的第一個該元素,remove(index)會刪除該下標的元素。調用一次remove方法,會使list中的所有該刪除元素后面的元素前移。看看一個remove(Object obj)的源碼:


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

  remove是可以刪除null的,但一般情況都是走else路徑,再看看faseRemove方法:


	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; // Let gc do its work 
	}大專欄  List.remove()的使用注意>

  System.arraycopy就是將list中刪除元素的后面遷移,暫時沒繼續深入了解。總得來說,這樣的操作是因為沒有對變化的list做出相應的下標處理而產生了錯誤的結果,但是並沒有產生異常。

使用forEach循環

  錯誤的使用代碼如下:


public static void main(String [] args){
        List<String> list = new ArrayList<String>();
        list.add("111");
        list.add("111");
        list.add("111");
        list.add("333");
        list.add("333");
        for (String str : list){
            if("111".equals(str)){
                list.remove(str);//拋出異常java.util.ConcurrentModificationException
            }
        }
    }

  這樣使用會拋出java.util.ConcurrentModificationException。在forEach中,遍歷的集合都必須實現Iterable接口(數組除外)。而forEach的寫法實際是對Iterator遍歷的簡寫,類似於以下代碼:


public void display(){  
        for(String s : strings){  
            System.out.println(s);  
        }  
          
        Iterator<String> iterator = strings.iterator();  
        while(iterator.hasNext()){  
            String s = iterator.next();  
            System.out.println(s);  
        }  
    }

  上面的forEach和下面的Iterator迭代器效果一樣,涉及到編譯原理的一些內容,就不深究了。可以理解為forEach將其中的集合轉為了迭代器進行遍歷,而該迭代器內部有一個next()方法,代碼如下:


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

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

  可以看到在使用next()方法獲取下一個元素的之前會先檢查迭代器修改次數,在我們使用ArrayList的remove()方法刪除元素時,實際只修改了modCount,這樣就會造成modCount和expectedModCount不相等,從而拋出異常。而使用迭代器本身的remove()方法則不會,因為Iterator本身的remove()方法會同時修改modCount和expectedModCount。

  引用一段網上的解釋:

  Iterator是工作在一個獨立的線程中,並且擁有一個mutex鎖。 Iterator被創建之后會建立一個指向原來對象的單鏈索引表,當原來的對象數量發生變化時,這個索引表的內容不會同步改變,所以當索引指針往后移動的時候就找不到要迭代的對象,所以按照fail-fast原則Iterator會馬上拋出java.util.ConcurrentModificationException異常。所以Iterator在工作的時候是不允許被迭代的對象被改變的。但你可以使用Iterator本身的方法remove()來刪除對象,Iterator.remove() 方法會在刪除當前迭代對象的同時維護索引的一致性。

  最后總結一下就是,forEach將List轉為了Iterator,刪除元素就需要使用Iterator的remove()方法,錯誤地使用了List.remove()方法就會拋出異常。

參考

  Java中ArrayList循環遍歷並刪除元素的陷阱

  List集合remove元素的問題

  Java語法糖1:可變長度參數以及foreach循環原理

  java forEach實現原理


免責聲明!

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



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