ArrayList循環遍歷並刪除元素的常見陷阱


在工作和學習中,經常碰到刪除ArrayList里面的某個元素,看似一個很簡單的問題,卻很容易出bug。不妨把這個問題當做一道面試題目,我想一定能難道不少的人。今天就給大家說一下在ArrayList循環遍歷並刪除元素的問題。首先請看下面的例子:

import java.util.ArrayList;
public class ArrayListRemove
{
  publicstaticvoidmain(String[]args)
  {
    ArrayList<String>list=newArrayList<String>();
    list.add("a");
    list.add("b");
    list.add("b");
    list.add("c");
    list.add("c");
    list.add("c");
    remove(list);
    for(Strings:list)
    {
      System.out.println("element : "+s);
    }
  }
  public static void remove(ArrayList<String> list)
  {
  // TODO:
  }
}

如果要想刪除list的b字符,有下面兩種常見的錯誤例子:

錯誤寫法實例一:

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

錯誤的原因:這種最普通的循環寫法執行后會發現第二個“b”的字符串沒有刪掉。

錯誤寫法實例二:

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

錯誤的原因:這種for-each寫法會報出著名的並發修改異常:java.util.ConcurrentModificationException。

先解釋一下實例一的錯誤原因。翻開JDK的ArrayList源碼,先看下ArrayList中的remove方法(注意ArrayList中的remove有兩個同名方法,只是入參不同,這里看的是入參為Object的remove方法)是怎么實現的:

public boolean remove(Objecto){
    if(o==null){
        for(intindex=0;index<size;index++)
            if(elementData[index]==null){
                fastRemove(index);
                return true;
            }
            }else{
                for(intindex=0;index<size;index++)
                if(o.equals(elementData[index])){
                fastRemove(index);
                return true;
            }
    }
    return false;
}            

一般情況下程序的執行路徑會走到else路徑下最終調用faseRemove方法:

private void fastRemove(int index){
        modCount++;
        intnumMoved=size-index-1;
        if(numMoved>0)             
          System.arraycopy(elementData,index+1,elementData,index,numMoved);
        elementData[--size]=null;// Let gc do its work
}

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

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

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

接着解釋一下實例二的錯誤原因。錯誤二產生的原因卻是foreach寫法是對實際的Iterable、hasNext、next方法的簡寫,問題同樣處在上文的fastRemove方法中,可以看到第一行把modCount變量的值加一,但在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();
}

這里會做迭代器內部修改次數檢查,因為上面的remove(Object)方法修改了modCount的值,所以才會報出並發修改異常。要避免這種情況的出現則在使用迭代器迭代時(顯示或for-each的隱式)不要使用ArrayList的remove,改為用Iterator的remove即可。

public static void remove(ArrayList<String> list) 
    {
        Iterator<String> it = list.iterator();
        while (it.hasNext()) 
        {
            String s = it.next();
            if (s.equals("b")) 
            {
                it.remove();
            }
        }
}


免責聲明!

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



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