1. 程式人生 > >為什麽禁止在 foreach 循環裏進行元素的 remove/add 操作

為什麽禁止在 foreach 循環裏進行元素的 remove/add 操作

詳細 控制 string 得到 each lec 就是 編譯 分享

  首先看下邊一個例子,展示了正確的做法和錯誤的錯發:

技術分享圖片

  這是為什麽呢,具體原因下面進行詳細說明:

1、foreach循環(Foreach loop)是計算機編程語言中的一種控制流程語句,通常用來循環遍歷數組或集合中的元素。Java語言從JDK 1.5.0開始引入foreach循環。在遍歷數組、集合方面,foreach為開發人員提供了極大的方便。通常也被稱之為增強for循環。其實,增強for循環也是Java給我們提供的一個語法糖,如果將以上代碼編譯後的class文件進行反編譯(使用jad工具)的話,可以得到以下代碼:

 1 Iterator iterator = userNames.iterator();
2 do 3 { 4 if(!iterator.hasNext()) 5 break; 6 String userName = (String)iterator.next(); 7 if(userName.equals("Hollis")) 8 userNames.remove(userName); 9 } while(true); 10 System.out.println(userNames);

  可以發現,原本的增強for循環,其實是依賴了while循環和Iterator實現的。

  那麽,我們再看下,如果使用增強for循環的話會發生什麽:

 1 List<String> userNames = new ArrayList<String>() {{
 2     add("Hollis");
 3     add("hollis");
 4     add("HollisChuang");
 5     add("H");
 6 }};
 7 
 8 for (String userName : userNames) {
 9     if (userName.equals("Hollis")) {
10         userNames.remove(userName);
11
} 12 } 13 14 System.out.println(userNames);

  以上代碼,使用增強for循環遍歷元素,並嘗試刪除其中的Hollis字符串元素。運行以上代碼,會拋出以下異常:

1 java.util.ConcurrentModificationException

  之所以會出現這個異常,是因為觸發了一個Java集合的錯誤檢測機制——fail-fast

2、接下來,我們就來分析下在增強for循環中add/remove元素的時候會拋出java.util.ConcurrentModificationException的原因,即解釋下到底什麽是fail-fast進制,fail-fast的原理等。

  fail-fast,即快速失敗,它是Java集合的一種錯誤檢測機制。當多個線程對集合(非fail-safe的集合類)進行結構上的改變的操作時,有可能會產生fail-fast機制,這個時候就會拋出ConcurrentModificationException(當方法檢測到對象的並發修改,但不允許這種修改時就拋出該異常)。

  同時需要註意的是,即使不是多線程環境,如果單線程違反了規則,同樣也有可能會拋出改異常。

  那麽,在增強for循環進行元素刪除,是如何違反了規則的呢?

  (1)Iterator.next 調用了 Iterator.checkForComodification方法 ,而異常就是checkForComodification方法中拋出的。

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

  (2)modCount是ArrayList中的一個成員變量。它表示該集合實際被修改的次數,remove方法它只修改了modCount,並沒有對expectedModCount做任何操作,所以導致拋出java.util.ConcurrentModificationException異常

  簡單總結一下,之所以會拋出ConcurrentModificationException異常,是因為我們的代碼中使用了增強for循環,而在增強for循環中,集合遍歷是通過iterator進行的,但是元素的add/remove卻是直接使用的集合類自己的方法。這就導致iterator在遍歷的時候,會發現有一個元素在自己不知不覺的情況下就被刪除/添加了,就會拋出一個異常,用來提示用戶,可能發生了並發修改。

3、正確姿勢

  1、直接使用普通for循環進行操作

    我們說不能在foreach中進行,但是使用普通的for循環還是可以的,因為普通for循環並沒有用到Iterator的遍歷,所以壓根就沒有進行fail-fast的檢驗  

 1    List<String> userNames = new ArrayList<String>() {{
 2         add("Hollis");
 3         add("hollis");
 4         add("HollisChuang");
 5         add("H");
 6     }};
 7 
 8     for (int i = 0; i < 1; i++) {
 9         if (userNames.get(i).equals("Hollis")) {
10             userNames.remove(i);
11         }
12     }
13     System.out.println(userNames);

  2、直接使用Iterator進行操作

    除了直接使用普通for循環以外,我們還可以直接使用Iterator提供的remove方法,如果直接使用Iterator提供的remove方法,那麽就可以修改到expectedModCount的值。那麽就不會再拋出異常了。其實現代碼如下:

 1     List<String> userNames = new ArrayList<String>() {{
 2         add("Hollis");
 3         add("hollis");
 4         add("HollisChuang");
 5         add("H");
 6     }};
 7 
 8     Iterator iterator = userNames.iterator();
 9 
10     while (iterator.hasNext()) {
11         if (iterator.next().equals("Hollis")) {
12             iterator.remove();
13         }
14     }
15     System.out.println(userNames);

  3、使用Java 8中提供的filter過濾

    Java 8中可以把集合轉換成流,對於流有一種filter操作, 可以對原始 Stream 進行某項測試,通過測試的元素被留下來生成一個新 Stream。

1     List<String> userNames = new ArrayList<String>() {{
2         add("Hollis");
3         add("hollis");
4         add("HollisChuang");
5         add("H");
6     }};
7 
8     userNames = userNames.stream().filter(userName -> !userName.equals("Hollis")).collect(Collectors.toList());
9     System.out.println(userNames);

  4、直接使用fail-safe的集合類

    在Java中,除了一些普通的集合類以外,還有一些采用了fail-safe機制的集合類。這樣的集合容器在遍歷時不是直接在集合內容上訪問的,而是先復制原有集合內容,在拷貝的集合上進行遍歷。

    由於叠代時是對原集合的拷貝進行遍歷,所以在遍歷過程中對原集合所作的修改並不能被叠代器檢測到,所以不會觸發ConcurrentModificationException。 

 1 ConcurrentLinkedDeque<String> userNames = new ConcurrentLinkedDeque<String>() {{
 2     add("Hollis");
 3     add("hollis");
 4     add("HollisChuang");
 5     add("H");
 6 }};
 7 
 8 for (String userName : userNames) {
 9     if (userName.equals("Hollis")) {
10         userNames.remove();
11     }
12 }    

    基於拷貝內容的優點是避免了ConcurrentModificationException,但同樣地,叠代器並不能訪問到修改後的內容,即:叠代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發生的修改叠代器是不知道的。

    java.util.concurrent包下的容器都是安全失敗,可以在多線程下並發使用,並發修改。

  5、使用增強for循環其實也可以

    如果,我們非常確定在一個集合中,某個即將刪除的元素只包含一個的話, 比如對Set進行操作,那麽其實也是可以使用增強for循環的,只要在刪除之後,立刻結束循環體,不要再繼續進行遍歷就可以了,也就是說不讓代碼執行到下一次的next方法

 1     List<String> userNames = new ArrayList<String>() {{
 2         add("Hollis");
 3         add("hollis");
 4         add("HollisChuang");
 5         add("H");
 6     }};
 7 
 8     for (String userName : userNames) {
 9         if (userName.equals("Hollis")) {
10             userNames.remove(userName);
11             break;
12         }
13     }
14     System.out.println(userNames);

    以上這五種方式都可以避免觸發fail-fast機制,避免拋出異常。如果是並發場景,建議使用concurrent包中的容器,如果是單線程場景,Java8之前的代碼中,建議使用Iterator進行元素刪除,Java8及更新的版本中,可以考慮使用Stream及filter。

原文鏈接:https://mp.weixin.qq.com/s/zI0AnGYhkojNl4qVJZaCBQ

為什麽禁止在 foreach 循環裏進行元素的 remove/add 操作