foreach去除重复元素java_Java foreach 中List移除元素抛出ConcurrentModificationException原因全解析...
本文重點探討 foreach 循環中List 移除元素造成?java.util.ConcurrentModificationException 異常的原因。
先看《阿里巴巴 Java開發手冊》中的相關規定:
那么思考幾個問題:反例的運行結果怎樣?
造成這種現象的根本原因是什么?
有沒有更優雅地的移除元素姿勢?
本文將為你深度解讀該問題。
2.0 反例源代碼
public?class?ListExceptionDemo?{
public?static?void?main(String[]?args)?{
List?list?=?new?ArrayList<>();
list.add("1");
list.add("2");
for?(String?item?:?list)?{
if?("1".equals(item))?{
list.remove(item);
}
}
}
}
2.1 反例的運行結果
當 if 的判斷條件是 “1”.equals(item) 時,程序沒有拋出任何異常。if?("1".equals(item))?{
list.remove(item);
}
而當判斷條件是 :"2".equals(item)時,運行會報 java.util.ConcurrentModificationException。
2.2 原因分析
2.2.1 錯誤提示
既然報錯,那么好辦,直接看錯誤提示唄。Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.chujianyun.common.collection.list.ListExceptionDemo.main(ListExceptionDemo.java:13)
啥?ConcurrentModificationException? 并發修改異常? 一個線程哪來的并發呢?
對應的時序圖
然后我們通過錯誤提示看源碼:我們看到錯誤的原因是執行 ArrayList的 Itr.next 取下一個元素檢查 并發修改是public?E?next()?{
checkForComodification();
int?i?=?cursor;
if?(i?>=?size)
throw?new?NoSuchElementException();
Object[]?elementData?=?ArrayList.this.elementData;
if?(i?>=?elementData.length)
throw?new?ConcurrentModificationException();
cursor?=?i?+?1;
return?(E)?elementData[lastRet?=?i];
}
modCount 和 expectedModCount不一致導致的:final?void?checkForComodification()
{
if?(modCount?!=?expectedModCount)
throw?new?ConcurrentModificationException();
}
因此可以推測出發生異常的根本原因在于:取下一個元素時,檢查 modCount,發現不一致。
2.2.2 代碼調試法
為了驗證上面的推測,大家可以在上述兩個關鍵函數上打斷點,通過單步了解程序的運行步驟。
我們通過調試可以“觀察到”,ArrayList中的?foreach 循環的語法糖最終迭代器Array$Itr 實現的。
通過斷點我們發現,ArrayList 構造內部類 Itr 對象時?expectedModCount 的值為 ArrayList的 modCount。
運行 next 函數時會檢查List 中的 modCount 的值 和 構造迭代器時“備份的” expectedModCount 是否相等。
通過調試我們還發現:雖然原始 list 至于兩個元素,for each 循環執行兩次后,滿足if 條件移除 值為“2”的元素之后, foreach 循環依然可以進入,此時會再次通過 next 取出 list中的元素,又會執行? checkForComodification函數檢查上述兩個值是否相等,此時不等,拋出異常。
那么這里有存在兩個問題:為什么 List 為 2? , next 卻執行了 3 次呢?
如果不通過調試我們怎么知道 foreach 語法糖的底層如何實現的呢?
帶著這兩個問題,我們繼續深入研究下去。
2.2.3? 源碼解析
我們查看? ArrayList$Itr 的 hasNext 函數:private?class?Itr?implements?Iterator?{
int?cursor;???????//?index?of?next?element?to?return
int?lastRet?=?-1;?//?index?of?last?element?returned;?-1?if?no?such
int?expectedModCount?=?modCount;
Itr(){}
public?boolean?hasNext()?{
return?cursor?!=?size;
}
//?其他省略
}
發現ArrayList的迭代器判斷是否有下一個元素的標準是將下一個待返回的元素的索引和 size 比,不等表示還有下一個元素。
我們重新看源碼:public?static?void?main(String[]?args)?{
List?list?=?new?ArrayList<>();
list.add("1");
list.add("2");
for?(String?item?:?list)?{
if?("2".equals(item))?{
list.remove(item);
}
}
}
最初 List 中有兩個元素,expectedModCount ?值為2。
遍歷第一個時沒有走到if, 遍歷第二個元素時走到if ,通過 List.remove 函數移除了元素。public?boolean?remove(Object?o)?{
if?(o?==?null)?{
for?(int?index?=?0;?index?
if?(elementData[index]?==?null)?{
fastRemove(index);
return?true;
}
}?else?{
for?(int?index?=?0;?index?
if?(o.equals(elementData[index]))?{
fastRemove(index);
return?true;
}
}
return?false;
}
而remove會調用 fastRemove 函數實際移除掉元素,在此函數中會將 modCount+1,即 modCount的值為3。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;?//?clear?to?let?GC?do?its?work
}
因此在次進入foreach 時,expectedModCount ?值 和?modCount的值 不相等,因此認為還有下一個元素。
但是調用迭代器的 next 函數時需檢查兩者是相等,發現不等,拋出ConcurrentModificationException異常。
當 if條件是? “1”.equals(item)時public?static?void?main(String[]?args)?{
List?list?=?new?ArrayList<>();
list.add("1");
list.add("2");
for?(String?item?:?list)?{
if?("1".equals(item))?{
list.remove(item);
}
}
}
循環取出第一個元素后直接通過list給移除掉了,再次進入 foreach循環時,通過 hashNext 判斷是否有下一個元素時,由于 游標==1(此時list的 size),因此判斷沒下一個元素。
也就是說此時循環只執行了一次就結束了,沒有走到可以拋出ConcurrentModificationException異常的任何函數中,從而沒有任何錯誤。
讀到這里對迭代器的理解是不是又深了一層呢?
看到這里可能還有些同學對 foreach 究竟底層怎么實現的仍然一知半解,那么請看下一部分。
2.2.4 反匯編
話不多說,直接反匯編:public?class?com.chujianyun.common.collection.list.ListExceptionDemo?{
public?com.chujianyun.common.collection.list.ListExceptionDemo();
Code:
0:?aload_0
1:?invokespecial?#1??????????????????//?Method?java/lang/Object."":()V
4:?return
public?static?void?main(java.lang.String[]);
Code:
0:?new???????????#2??????????????????//?class?java/util/ArrayList
3:?dup
4:?invokespecial?#3??????????????????//?Method?java/util/ArrayList."":()V
7:?astore_1
8:?aload_1
9:?ldc???????????#4??????????????????//?String?1
11:?invokeinterface?#5,??2????????????//?InterfaceMethod?java/util/List.add:(Ljava/lang/Object;)Z
16:?pop
17:?aload_1
18:?ldc???????????#6??????????????????//?String?2
20:?invokeinterface?#5,??2????????????//?InterfaceMethod?java/util/List.add:(Ljava/lang/Object;)Z
25:?pop
26:?aload_1
27:?invokeinterface?#7,??1????????????//?InterfaceMethod?java/util/List.iterator:()Ljava/util/Iterator;
32:?astore_2
33:?aload_2
34:?invokeinterface?#8,??1????????????//?InterfaceMethod?java/util/Iterator.hasNext:()Z
39:?ifeq??????????72
42:?aload_2
43:?invokeinterface?#9,??1????????????//?InterfaceMethod?java/util/Iterator.next:()Ljava/lang/Object;
48:?checkcast?????#10?????????????????//?class?java/lang/String
51:?astore_3
52:?ldc???????????#6??????????????????//?String?2
54:?aload_3
55:?invokevirtual?#11?????????????????//?Method?java/lang/String.equals:(Ljava/lang/Object;)Z
58:?ifeq??????????69
61:?aload_1
62:?aload_3
63:?invokeinterface?#12,??2???????????//?InterfaceMethod?java/util/List.remove:(Ljava/lang/Object;)Z
68:?pop
69:?goto??????????33
72:?return
}
代碼偏移從 0 到 25 行實現下面這部分功能:List?list?=?new?ArrayList<>();
list.add("1");
list.add("2");
從 26行開始我們發現底層使用迭代器實現,我們腦補后翻譯回 Java代碼大致如下:public?static?void?main(String[]?args)?{
List?list?=?new?ArrayList<>();
list.add("1");
list.add("2");
Iterator?iterator?=?list.iterator();
while?(iterator.hasNext())?{
String?item?=?iterator.next();
if?("2".equals(item))?{
//iterator.remove();
list.remove(item);
}
}
}
大家運行“翻譯”后的代碼發信啊和原始代碼的報錯內容完全一致:Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.chujianyun.common.collection.list.ListException.main(ListException.java:16)
2.2.5 繼續深挖
1、為啥通過 iterator.remove() 移除元素就沒事呢?
我們看 java.util.ArrayList.Itr#remove 的源碼:public?void?remove()?{
if?(lastRet?
throw?new?IllegalStateException();
checkForComodification();
try?{
ArrayList.this.remove(lastRet);
cursor?=?lastRet;
lastRet?=?-1;
expectedModCount?=?modCount;
}?catch?(IndexOutOfBoundsException?ex)?{
throw?new?ConcurrentModificationException();
}
}
從這里我們看到,通過迭代器移除元素后, expectedModCount 會重新賦值為 modCount。
因此使用iterator.remove() 移除元素不報錯的原因就找到了。
2、有沒有比手冊給出的代碼更優雅的寫法?
我們打開其函數列表,觀察List 和其父類有沒有便捷地移除元素方式:
“驚奇”地發現,Collection 接口提供了 removeIf 函數可以滿足此需求。
還等啥呢,替換下,發現代碼如此簡潔:public?static?void?main(String[]?args)?{
List?list?=?new?ArrayList<>();
list.add("1");
list.add("2");????????//?一行代碼實現
list.removeIf("2"::equals);
}
自此是不是文章就該結束了呢?
NO..
removeIf 為啥能夠實現移除元素的功能呢?
我們猜測,底層應該是遍歷然后對比元素然后移除,可能也是迭代器方式,我們看源碼:
java.util.Collection#removeIfdefault?boolean?removeIf(Predicate?super?E>?filter)?{
Objects.requireNonNull(filter);
boolean?removed?=?false;
final?Iterator?each?=?iterator();
while?(each.hasNext())?{
if?(filter.test(each.next()))?{
each.remove();
removed?=?true;
}
}
return?removed;
}
我們發現和我們想的比較一致。
本小節對《阿里巴巴 Java開發手冊》中 foreach 循環 List 移除元素導致并發修改異常的問題,進行了全面深入地剖析。
希望可以幫助大家,徹底搞懂這個問題。
另外也提供了研究類似問題的一般思路,即代碼調試、讀源碼、反匯編等。
通過這個問題,希望大家遇到問題時,能夠養成深挖的精神,通過問題帶動知識的理解,知其所以然。
最后提醒大家,不要看書記結論,容易忘,記住不會用,要多思考原因,才能理解更深刻。
“盡信書不如無書”,不要認為作者寫的都是對的,都是最好的,要有自己的思考。
想了解更多《手冊》詳解的更多內容,想學習更多開發和避坑技巧等,請關注《阿里巴巴Java 開發手冊》詳解專欄。
總結
以上是生活随笔為你收集整理的foreach去除重复元素java_Java foreach 中List移除元素抛出ConcurrentModificationException原因全解析...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java 字符串用法_java中字符串的
- 下一篇: java线程中notify_Java多线