Java面試題總結(一)之Java中ArrayList循環遍歷並刪除元素的陷阱

面試題:刪除列表中等於bb的元素

先看測試案例:

    import java.util.ArrayList;  

    public class ArrayListRemove {  

        public static void main(String[] args) {  
            ArrayList<String> list = new ArrayList<String>();  
            list.add("a");  
            list.add("bb");  
            list.add("bb");  
            list.add("ccc");  
            list.add("ccc");  
            list.add("ccc");  

            remove(list);  

            for (String s : list) {  
                System.out.println("element : " + s);  
            }  
        }  

        public static void remove(ArrayList<String> list) {  
            // TODO:  
        }  
    }  

錯誤寫法示例一:

public static void remove(ArrayList<String> list) {  
    for (int i = 0; i < list.size(); i++) {  
        String s = list.get(i);  
        if (s.equals("bb")) {  
            list.remove(s);  
        }  
    }  
} 

這種最普通的循環寫法執行後會發現有一個“bb”的字符串沒有刪掉。

錯誤寫法示例二:

    public static void remove(ArrayList<String> list) {  
        for (String s : list) {  
            if (s.equals("bb")) {  
                list.remove(s);  
            }  
        }  
    }  

這種for each寫法會發現報出著名的併發修改異常Java.util.ConcurrentModificationException。

要分析產生上述錯誤現象的原因唯有看看JDK的ArrayList的源碼,先看下ArrayList中的remove方法(注意ArrayList中的remove有兩個同名方法,只是輸入參數不同,這裏看的是輸入參數是Object的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])) {  
                    fastRemove(index);  
                    return true;  
                }  
        }  
        return false;  
    }  

按一般執行路徑會走到else路徑下最終調用fastRemove(index)方法;

    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  
    }  

可以看到會執行System.arraycopy方法,導致刪除元素時涉及到數組元素的移動。針對錯誤寫法一,在遍歷第二個元素字符串bb時因爲符合刪除條件,所以將該元素從數組中刪除,並且將後一個元素移動(也是字符串bb)至當前位置,導致下一次循環遍歷時後一個字符串bb並沒有遍歷到,所以無法刪除。針對這種情況可以倒序刪除的方式來避免。 
解決辦法如下:

public static void remove(ArrayList<String> list){
    for(int i=list.size()-1;i>=0;i--){
        String s=list.get(i);
        if(s.equals("bb")){
            list.remove(s);
        }
    }
}

因爲數組倒序遍歷時即使發生元素刪除也不影響後序元素遍歷。

而錯誤二產生的原因卻是for each增強for循環寫法是對實際的Iterator、hasNext、next方法的簡寫,問題同樣處在上文的fastRemove中,可以看到第一行把modCount變量的值加1,但在ArrayList返回的迭代器(該代碼在其父類AbstractList中)

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

這裏返回的是AbstractList類內部的迭代器實現private class Itr implements Iterator ,看這個類的next方法:

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

第一行checkForComodification方法:

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

總結: 

錯誤原因都是ArrayList集合中remove方法底層的源碼中有一個fastRemove(index)方法,然後會有一個modCount++的操作,然後在ArratList內部的迭代器中有一個checkForComodification操作,也就是檢查modCount是否改變,如果改變了,就拋出併發修改錯誤。 同樣的在For each增強for循環中,也是利用了ArrayList自身的Iterator迭代器,也是會出現這樣的錯誤。

對於一般的for遍歷,可能並沒有刪除要修改的數,可以採用倒序刪除的寫法改正這個錯誤。 對於增強for循環中的遍歷,會拋出併發修改異常,使用Iterator自己的remove方法。

從今天開始我增加分類Java面試題,我轉發記錄學習得到的知識點,大家一起學習,一起進步!加油

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章