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


ava中的ArrayList循環遍歷並且刪除元素時經常不小心掉坑里,昨天又碰到了,感覺有必要單獨寫篇文章記一下。

 

先寫個測試代碼:

Java代碼   收藏代碼
  1. import java.util.ArrayList;  
  2.   
  3. public class ArrayListRemove {  
  4.   
  5.     public static void main(String[] args) {  
  6.         ArrayList<String> list = new ArrayList<String>();  
  7.         list.add("a");  
  8.         list.add("bb");  
  9.         list.add("bb");  
  10.         list.add("ccc");  
  11.         list.add("ccc");  
  12.         list.add("ccc");  
  13.   
  14.         remove(list);  
  15.   
  16.         for (String s : list) {  
  17.             System.out.println("element : " + s);  
  18.         }  
  19.     }  
  20.   
  21.     public static void remove(ArrayList<String> list) {  
  22.         // TODO:  
  23.     }  
  24. }  

 

錯誤寫法示例一:

Java代碼   收藏代碼
  1. public static void remove(ArrayList<String> list) {  
  2.     for (int i = 0; i < list.size(); i++) {  
  3.         String s = list.get(i);  
  4.         if (s.equals("bb")) {  
  5.             list.remove(s);  
  6.         }  
  7.     }  
  8. }  

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

 

錯誤寫法示例二:

Java代碼   收藏代碼
  1. public static void remove(ArrayList<String> list) {  
  2.     for (String s : list) {  
  3.         if (s.equals("bb")) {  
  4.             list.remove(s);  
  5.         }  
  6.     }  
  7. }  

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

 

要分析產生上述錯誤現象的原因唯有翻一翻jdk的ArrayList源碼,先看下ArrayList中的remove方法(注意ArrayList中的remove有兩個同名方法,只是入參不同,這里看的是入參為Object的remove方法)是怎么實現的:

Java代碼   收藏代碼
  1. public boolean remove(Object o) {  
  2.     if (o == null) {  
  3.         for (int index = 0; index < size; index++)  
  4.             if (elementData[index] == null) {  
  5.                 fastRemove(index);  
  6.                 return true;  
  7.             }  
  8.     } else {  
  9.         for (int index = 0; index < size; index++)  
  10.             if (o.equals(elementData[index])) {  
  11.                 fastRemove(index);  
  12.                 return true;  
  13.             }  
  14.     }  
  15.     return false;  
  16. }  

按一般執行路徑會走到else路徑下最終調用faseRemove方法:

Java代碼   收藏代碼
  1. private void fastRemove(int index) {  
  2.     modCount++;  
  3.     int numMoved = size - index - 1;  
  4.     if (numMoved > 0)  
  5.         System.arraycopy(elementData, index+1, elementData, index,  
  6.                          numMoved);  
  7.     elementData[--size] = null; // Let gc do its work  
  8. }  

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

針對這種情況可以倒序刪除的方式來避免:

Java代碼   收藏代碼
  1. public static void remove(ArrayList<String> list) {  
  2.     for (int i = list.size() - 1; i >= 0; i--) {  
  3.         String s = list.get(i);  
  4.         if (s.equals("bb")) {  
  5.             list.remove(s);  
  6.         }  
  7.     }  
  8. }  

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

 

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

Java代碼   收藏代碼
  1. public Iterator<E> iterator() {  
  2.     return new Itr();  
  3. }  

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

Java代碼   收藏代碼
  1. public E next() {  
  2.     checkForComodification();  
  3.     try {  
  4.         E next = get(cursor);  
  5.         lastRet = cursor++;  
  6.         return next;  
  7.     } catch (IndexOutOfBoundsException e) {  
  8.         checkForComodification();  
  9.         throw new NoSuchElementException();  
  10.     }  
  11. }  

第一行checkForComodification方法:

Java代碼   收藏代碼
  1. final void checkForComodification() {  
  2.     if (modCount != expectedModCount)  
  3.         throw new ConcurrentModificationException();  
  4. }  

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

Java代碼   收藏代碼
  1. public static void remove(ArrayList<String> list) {  
  2.     Iterator<String> it = list.iterator();  
  3.     while (it.hasNext()) {  
  4.         String s = it.next();  
  5.         if (s.equals("bb")) {  
  6.             it.remove();  
  7.         }  
  8.     }  
  9. }  

 


免責聲明!

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



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