foreach去除重复元素java_Java foreach 中List移除元素抛出ConcurrentModificationException原因全解析...
本文重點(diǎn)探討 foreach 循環(huán)中List 移除元素造成?java.util.ConcurrentModificationException 異常的原因。
先看《阿里巴巴 Java開發(fā)手冊(cè)》中的相關(guān)規(guī)定:
那么思考幾個(gè)問題:反例的運(yùn)行結(jié)果怎樣?
造成這種現(xiàn)象的根本原因是什么?
有沒有更優(yōu)雅地的移除元素姿勢(shì)?
本文將為你深度解讀該問題。
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 反例的運(yùn)行結(jié)果
當(dāng) if 的判斷條件是 “1”.equals(item) 時(shí),程序沒有拋出任何異常。if?("1".equals(item))?{
list.remove(item);
}
而當(dāng)判斷條件是 :"2".equals(item)時(shí),運(yùn)行會(huì)報(bào) java.util.ConcurrentModificationException。
2.2 原因分析
2.2.1 錯(cuò)誤提示
既然報(bào)錯(cuò),那么好辦,直接看錯(cuò)誤提示唄。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? 并發(fā)修改異常? 一個(gè)線程哪來的并發(fā)呢?
對(duì)應(yīng)的時(shí)序圖
然后我們通過錯(cuò)誤提示看源碼:我們看到錯(cuò)誤的原因是執(zhí)行 ArrayList的 Itr.next 取下一個(gè)元素檢查 并發(fā)修改是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不一致導(dǎo)致的:final?void?checkForComodification()
{
if?(modCount?!=?expectedModCount)
throw?new?ConcurrentModificationException();
}
因此可以推測(cè)出發(fā)生異常的根本原因在于:取下一個(gè)元素時(shí),檢查 modCount,發(fā)現(xiàn)不一致。
2.2.2 代碼調(diào)試法
為了驗(yàn)證上面的推測(cè),大家可以在上述兩個(gè)關(guān)鍵函數(shù)上打斷點(diǎn),通過單步了解程序的運(yùn)行步驟。
我們通過調(diào)試可以“觀察到”,ArrayList中的?foreach 循環(huán)的語法糖最終迭代器Array$Itr 實(shí)現(xiàn)的。
通過斷點(diǎn)我們發(fā)現(xiàn),ArrayList 構(gòu)造內(nèi)部類 Itr 對(duì)象時(shí)?expectedModCount 的值為 ArrayList的 modCount。
運(yùn)行 next 函數(shù)時(shí)會(huì)檢查L(zhǎng)ist 中的 modCount 的值 和 構(gòu)造迭代器時(shí)“備份的” expectedModCount 是否相等。
通過調(diào)試我們還發(fā)現(xiàn):雖然原始 list 至于兩個(gè)元素,for each 循環(huán)執(zhí)行兩次后,滿足if 條件移除 值為“2”的元素之后, foreach 循環(huán)依然可以進(jìn)入,此時(shí)會(huì)再次通過 next 取出 list中的元素,又會(huì)執(zhí)行? checkForComodification函數(shù)檢查上述兩個(gè)值是否相等,此時(shí)不等,拋出異常。
那么這里有存在兩個(gè)問題:為什么 List 為 2? , next 卻執(zhí)行了 3 次呢?
如果不通過調(diào)試我們?cè)趺粗?foreach 語法糖的底層如何實(shí)現(xiàn)的呢?
帶著這兩個(gè)問題,我們繼續(xù)深入研究下去。
2.2.3? 源碼解析
我們查看? ArrayList$Itr 的 hasNext 函數(shù):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;
}
//?其他省略
}
發(fā)現(xiàn)ArrayList的迭代器判斷是否有下一個(gè)元素的標(biāo)準(zhǔn)是將下一個(gè)待返回的元素的索引和 size 比,不等表示還有下一個(gè)元素。
我們重新看源碼: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 中有兩個(gè)元素,expectedModCount ?值為2。
遍歷第一個(gè)時(shí)沒有走到if, 遍歷第二個(gè)元素時(shí)走到if ,通過 List.remove 函數(shù)移除了元素。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會(huì)調(diào)用 fastRemove 函數(shù)實(shí)際移除掉元素,在此函數(shù)中會(huì)將 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
}
因此在次進(jìn)入foreach 時(shí),expectedModCount ?值 和?modCount的值 不相等,因此認(rèn)為還有下一個(gè)元素。
但是調(diào)用迭代器的 next 函數(shù)時(shí)需檢查兩者是相等,發(fā)現(xiàn)不等,拋出ConcurrentModificationException異常。
當(dāng) if條件是? “1”.equals(item)時(shí)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);
}
}
}
循環(huán)取出第一個(gè)元素后直接通過list給移除掉了,再次進(jìn)入 foreach循環(huán)時(shí),通過 hashNext 判斷是否有下一個(gè)元素時(shí),由于 游標(biāo)==1(此時(shí)list的 size),因此判斷沒下一個(gè)元素。
也就是說此時(shí)循環(huán)只執(zhí)行了一次就結(jié)束了,沒有走到可以拋出ConcurrentModificationException異常的任何函數(shù)中,從而沒有任何錯(cuò)誤。
讀到這里對(duì)迭代器的理解是不是又深了一層呢?
看到這里可能還有些同學(xué)對(duì) foreach 究竟底層怎么實(shí)現(xiàn)的仍然一知半解,那么請(qǐng)看下一部分。
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 行實(shí)現(xiàn)下面這部分功能:List?list?=?new?ArrayList<>();
list.add("1");
list.add("2");
從 26行開始我們發(fā)現(xiàn)底層使用迭代器實(shí)現(xiàn),我們腦補(bǔ)后翻譯回 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);
}
}
}
大家運(yùn)行“翻譯”后的代碼發(fā)信啊和原始代碼的報(bào)錯(cuò)內(nèi)容完全一致: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 繼續(xù)深挖
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 會(huì)重新賦值為 modCount。
因此使用iterator.remove() 移除元素不報(bào)錯(cuò)的原因就找到了。
2、有沒有比手冊(cè)給出的代碼更優(yōu)雅的寫法?
我們打開其函數(shù)列表,觀察List 和其父類有沒有便捷地移除元素方式:
“驚奇”地發(fā)現(xiàn),Collection 接口提供了 removeIf 函數(shù)可以滿足此需求。
還等啥呢,替換下,發(fā)現(xiàn)代碼如此簡(jiǎn)潔:public?static?void?main(String[]?args)?{
List?list?=?new?ArrayList<>();
list.add("1");
list.add("2");????????//?一行代碼實(shí)現(xiàn)
list.removeIf("2"::equals);
}
自此是不是文章就該結(jié)束了呢?
NO..
removeIf 為啥能夠?qū)崿F(xiàn)移除元素的功能呢?
我們猜測(cè),底層應(yīng)該是遍歷然后對(duì)比元素然后移除,可能也是迭代器方式,我們看源碼:
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;
}
我們發(fā)現(xiàn)和我們想的比較一致。
本小節(jié)對(duì)《阿里巴巴 Java開發(fā)手冊(cè)》中 foreach 循環(huán) List 移除元素導(dǎo)致并發(fā)修改異常的問題,進(jìn)行了全面深入地剖析。
希望可以幫助大家,徹底搞懂這個(gè)問題。
另外也提供了研究類似問題的一般思路,即代碼調(diào)試、讀源碼、反匯編等。
通過這個(gè)問題,希望大家遇到問題時(shí),能夠養(yǎng)成深挖的精神,通過問題帶動(dòng)知識(shí)的理解,知其所以然。
最后提醒大家,不要看書記結(jié)論,容易忘,記住不會(huì)用,要多思考原因,才能理解更深刻。
“盡信書不如無書”,不要認(rèn)為作者寫的都是對(duì)的,都是最好的,要有自己的思考。
想了解更多《手冊(cè)》詳解的更多內(nèi)容,想學(xué)習(xí)更多開發(fā)和避坑技巧等,請(qǐng)關(guān)注《阿里巴巴Java 開發(fā)手冊(cè)》詳解專欄。
總結(jié)
以上是生活随笔為你收集整理的foreach去除重复元素java_Java foreach 中List移除元素抛出ConcurrentModificationException原因全解析...的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java 字符串用法_java中字符串的
- 下一篇: java线程中notify_Java多线