前言

前陣子,一名java初學者 遇到了list 使用remove的問題,當時我暫且給他說了一種解決方案,
事后,我細想,
是不是很多初學者都會碰到這種問題?
雖然阿里開發手冊里面有說到這個坑,但是是不是每個人都清楚?
這個錯誤的出現原由是什么?
怎么避免?怎么解決?
只能使用迭代器iterator 方式嗎?
removeAll ? stream?removeIf ?
這篇文章里, 上面的種種疑問,都會涉及,但不限于,
因為我經常寫著寫著就扯遠了,可能會說到一些其他東西,
正文
跟著我的思路走,耐心讀完,沒有識訓你直接打我,
有個list :
List<String> list = new ArrayList(); list.add("C"); list.add("A"); list.add("B"); list.add("C"); list.add("F"); list.add("C"); list.add("C");
[C, A, B, C, F, C, C]
怎么移除掉list里面的某個元素呢 ?
list里面給我們提供了4個方法 :

先看 remove ( Object o) :
這個方面字面意思看,就是,你想移除list里面的哪個 Object ,你傳進來就可以,
看原始碼,如下圖:

也就是說并不是想移除哪個傳哪個就能移除完, 而僅僅是只移除首個符合規則的元素,
結合例子:
現在這個List里面,存在4 個 "C" 元素 , 使用remove("C"):
List<String> list = new ArrayList();
list.add("C");
list.add("A");
list.add("C");
list.add("B");
list.add("F");
list.add("C");
list.add("C");
System.out.println("未移除前"+list.toString());
list.remove("C");
System.out.println("移除后"+list.toString());
結果:
未移除前[C, A, C, B, F, C, C]
移除后[A, C, B, F, C, C]
所以,光這樣使用remove是不行的,不能實作我們需求 : 移除 list中的所有符合條件的元素,僅僅移除了符合條件的第一個 元素了,
這時候,大家可能就會想,那么我們就回圈+remove唄,這樣就能把每個符合條件的移除了,
真的嗎?
接著看,
回圈 + remove ( Object o)/ remove(Index i):
沒錯,我們可以配合回圈,把list里面的“C”元素都移除,
回圈自然有分 while回圈和 for回圈(包含foreach) ,
先看 foreach方式 :
不得行! 切記!
for (String str: list){
if ("C".equals(str)){
list.remove(str);
}
}
代碼看似沒問題,但是在foreach 使用 list的 remove / add 方法都是不行的!
報錯:
ConcurrentModificationException : 并發例外
PS: 其實如果大家曾閱讀過阿里的開發規范,也許會有一點印象,
7.【強制】不要在foreach回圈里進行元素的remove/add 操作,remove元素請使用Iterator方式,如果并發操作,需要對Iterator物件加鎖,
那么先不管,如果你閱讀過,可能也不一定知道里面的原理,所以繼續往下看吧,
在分析這個錯誤前,我來提一嘴 ,一部分的ArrayList的特性:
ArrayList是基于陣列實作的,是一個動態陣列,其容量能自動增長,
ArrayList不是執行緒安全的,
支持快速隨機訪問,通過下標序號index進行快速訪問,
接下來,跟著我,一起來分析這個報錯的出現 (當然我的挖錯方式不一定適合大家,但是也可以參考):
1. 分析出錯的代碼段
for (String str: list){
if ("C".equals(str)){
list.remove(str);
}
}
光這樣看,我們只能知道,用了foreach的語法糖,那么我們看編譯后的:

再看我們的報錯資訊:

原始碼分析:
通過我們反編譯的代碼,結合ArrayList的原始碼, 我們可以知道,
Itr 就是ArrayList里面的內部類,
而foreach的語法糖其實就是幫我們 new了一下 Itr,先呼叫hashNext()
while(var2.hasNext())
顯然是作為回圈的條件,那么我們也一起來簡單看下這個方法原始碼:
public boolean hasNext() {
return cursor != size;
}
size是啥?

那cursor是啥?

所以,hashNext() 意思是, 當cursor 不等于 size的時候,代表 還有下一位,繼續回圈就完事了,這個值其實不是本次報錯的重點,
我們繼續看 Itr的next()方法中的 checkForComodification()方法,就是這玩意導致報錯的,
那么我們直接定位到 checkForComodification()方法的原始碼:

代碼簡單, 也看到了我們剛才報的錯 ConcurrentModificationException 在里面躺著,
只要modCount 不等于 expectedModCount ,就拋錯,
那么我們就得明白 modCount 和 expectedModCount是啥?
expectedModCount簡單,是Itr里的一個屬性 ,在初始化的時候,就已經把 modCount的值 等賦給了 expectedModCount,

其實 expectedModCount 就是用來記錄 一開始 迭代的 list的 變更數modCount, 至于 list的 變更數modCount是啥,我們接著看,
點進去看modCount的原始碼:
可以看到作者真是苦口婆心,這么一個欄位屬性,人家寫了這么多注釋, 那肯定是解釋得非常細致了,
那么我來抽出一些核心的 翻譯一下,給各位看看:
此串列在結構上被修改的次數,結構修改是指改變結構尺寸的修改,
如果此欄位的值意外更改,則迭代器(或串列迭代器)將在
對{@code next}、{@code remove}、{@code previous}的回應,{@code set}或{@code add}操作,這提供了<i>快速失敗</i>行為,而不是迭代程序中并發修改的情況,
我來簡單再說一下:
這個modCount,可以理解為記錄list的變動值, 如果你的list里面連續add 7個元素,那么這個變動值就是7 . 如果是add 7個元素,remove 1個元素, 那么這個值就是8 . 反正就是修改變動的次數的一個統計值,
而這個值,在使用迭代的時候,會在迭代器初始化傳入,賦值給到迭代器 Itr 里面的內部記錄值 ,也就是我們剛剛講到的 expectedModCount 值 , 這樣來防止使用的時候,有意外的修改,導致并發的問題,
這么一說,其實我們報錯ConcurrentModificationException 的原因就很明顯了,
一開始的情況:

所以在我們第一次回圈檢測,使用foreach語法糖,呼叫 Itr的next()方法時,會去呼叫 check方法:
因為確實一開始大家都是7,檢測modCount和 expectedModCount值是通過的:

接著,我們繼續觸發 Itr的next()方法,按照往常,也是呼叫了check方法,結果檢測出來初始化傳入的list變化記錄數expectedModCount是7,而 最新的list的變更記錄數modCount 因為在第一次的list.remove觸發后,modCount++了,變成了8,所以: 
兩值不等, 拋出錯誤,
所以上述出現報錯 ConcurrentModificationException 的原因非常明了, 其實就是因為呼叫了 Itr的next()方法, 而next()方法每次執行時,會調check方法, 那么可以理解為,這是foreach語法糖+移除時的鍋,
那么我們就避免這個語法糖 ,我們先來個習慣性撰寫的for回圈方式:
List<String> list = new ArrayList();
list.add("C");
list.add("A");
list.add("C");
list.add("B");
list.add("F");
list.add("C");
list.add("C");
System.out.println("未移除前" + list.toString());
int size = list.size();
for (int i = 0; i < size; i++) {
if ("C".equals(list.get(i))){
list.remove("C");
}
}
System.out.println("移除后" + list.toString());
這樣的執行結果是啥, 報錯了,IndexOutOfBoundsException 陣列索引邊界例外:
為啥會錯啊,原因很簡單:
所以這個示例報錯的原由很簡單,我編碼問題,把size值提前固定為7了, 然后list的size是實時變化的,
那么我把size不提前獲取了,放在for回圈里面,這樣就不會導致 i++使 i大于list的size了:
List<String> list = new ArrayList();
list.add("C");
list.add("A");
list.add("C");
list.add("B");
list.add("F");
list.add("C");
list.add("C");
System.out.println("未移除前" + list.toString());
for (int i = 0; i < list.size(); i++) {
if ("C".equals(list.get(i))) {
list.remove("C");
}
}
System.out.println("移除后" + list.toString());
}
這樣的運行結果是什么:

雖然沒報錯,但是沒有移除干凈,為什么?
其實還是因為 list的size在真實的變動 ,每次移除,會讓size的值 -1 , 而 i 是一如既往的 +1 .
而因為ArrayList是陣列, 索引是連續的,每次移除,陣列的索引值都會 ’重新編排‘ 一次,
看個圖,我畫個簡單的例子給大家看看:

也就是說,其實每一次的remove變動, 因為我們的回圈 i值是一直 增加的,
所以會造成,我們想象的 陣列內第二個 C 元素 的索引是 2, 當i為2時會 拿出來檢測,這個假想時不對的,
因為如果 第二個 C 元素前面的 元素發生了變化, 那么它自己的索引也會往前 移動,
所以為什么會出現 移除不干凈的 現象 ,
其實簡單說就是 最后一個C元素因為前面的元素變動移除/新增,它的 index變化了,
然后i > list.size() 的時候就會 跳出回圈, 而這個倒霉蛋 C元素排在后面,index值在努力往前移,而 i 值在變大, 但是因為我們這邊是執行remove操作, list的size 在變小,
在 i值和 size值 兩個 交鋒相對的時候,最后一個C元素沒來得及匹對, i就已經大于 list.size ,導致回圈結束了,
這么說大家不知道能不能懂,因為對于初學者來說,可能沒那么快可以反應過來,
沒懂的兄弟,看我的文章,我決不會讓你帶著疑惑離開這篇文章,我再上個栗子,細說(已經理解的可以直接往下拉,跳過這段羅嗦的分析),
上栗子:
我們的list 里面 緊緊有 三個元素 "A" "C" "C" , 然后其余的不變,也是回圈里面移除”C“ 元素 ,
List<String> list = new ArrayList();
list.add("A");
list.add("C");
list.add("C");
System.out.println("未移除前" + list.toString());
for (int i = 0; i < list.size(); i++) {
if ("C".equals(list.get(i))) {
list.remove("C");
}
}
System.out.println("移除后" + list.toString());
先看一下結果,還是出現移除不干凈:
分析:
1. list的樣子:

2. 回圈觸發,第一次 i 的值為 0, 取出來的 元素是 A ,不符合要求:

3.繼續回圈, 此時list的size值 依然是不變,還是 3 ,而i的值因為 i++ 后變成了1 , 1 小于 3,條件符合,進入回圈內,取出 list里 index為 1的元素:
4.這個 C符合要求, 被移除, 移除后,我們的 list狀態變成了:
5. 此時此刻 list的 size 是 2 ,再一輪for回圈 , i 的值 i++ 后繼續變大,從1 變成了 2 , 2不小于 2 ,所以回圈結束了,
但是我們這時候list里面排在最后的那個C元素 原本index是 2,變成了index 1 ,這個家伙 都還沒被 取出來, 回圈結束了,它就逃過了檢測, 所以沒被移除干凈,
PS: 很可能有些看客 心里面會想(我YY你們會這么想), 平時用的remove是利用index移除的, 跟我上面使用的 remove(Object o) 還不一樣的,是不是我例子的代碼使用方法問題,
然而并不然,因為這個remove呼叫的是哪個,其實不是重點,看圖:

結果還是一樣:
其實 這樣的for回圈寫法, 跟 list的remove 到底使用的是 Object匹配移除 還是 Index移除 , 沒有關系的, 移除不干凈是因為 回圈 i的值 跟 list的size變動 ,跳出回圈造成的,
能看到這里的兄弟, 辛苦了,
那么 使用 remove 這個方法,結合回圈,那就真的沒辦法 移除干凈了嗎?
行得通的例子:
while回圈 :
List<String> list = new ArrayList();
list.add("C");
list.add("A");
list.add("C");
list.add("B");
list.add("F");
list.add("C");
list.add("C");
System.out.println("未移除前"+list.toString());
while (list.remove("C"));
System.out.println("移除后"+list.toString());
}
結果,完美執行:
為什么這么些 不會報ConcurrentModificationException錯,也不會報 IndexOutOfBoundsException 錯 呢?
我們看看編譯后的代碼:
可以看到時單純的呼叫list的remove方法而已,只要list里面有"C",那么移除回傳的就是true,那么就會繼續觸發再一次的remove(“C”),所以這樣下去,會把list里面的“C”都移除干凈,簡單看一眼原始碼:

所以這樣使用是行得通的,
那么當然還有文章開頭我給那位兄弟說的使用迭代器的方式動態洗掉也是行得通的:
Iterator
List<String> list = new ArrayList();
list.add("C");
list.add("A");
list.add("B");
list.add("C");
list.add("F");
list.add("C");
list.add("C");
System.out.println("未移除前" + list.toString());
Iterator<String> it = list.iterator();
while(it.hasNext()){
String x = it.next();
if("C".equals(x)){
it.remove();
}
}
System.out.println("移除后" + list.toString());
執行結果:
PS:
但是這個方式要注意的是, if判斷里面的順序,
一定要注意把 已知條件值前置 : "C".equals ( xxx) , 否則當我們的list內包含null 元素時, null是無法呼叫equals方法的,會拋出空指標例外,
那么其實我們如果真的想移除list里面的某個 元素,移除干凈 ,
我們其實 用removeAll ,就挺合適,
removeAll
list.removeAll(Collections.singleton("C"));
或者
list.removeAll(Arrays.asList("C"));
List<String> list = new ArrayList();
list.add("C");
list.add("A");
list.add("C");
list.add("B");
list.add("F");
list.add("C");
list.add("C");
System.out.println("未移除前" + list.toString());
list.removeAll(Collections.singleton("C"));
System.out.println("移除后" + list.toString());
運行結果:![]()
這里使用removeAll ,我想給大家提醒一點東西 !
list.removeAll(Collections.singleton("C"));
list.removeAll(Arrays.asList("C"));
這兩種寫法 運行移除 C 的時候都是沒問題的,
但是當list里面有 null 元素的時候,我們就得多加注意了, 我們想移除null元素的時候 ,先回顧一下 remove這個小方法,沒啥問題,使用得當即可:

意思是,remove 可以移除 空元素,也就是 null ,
但是我們看看 removeAll :


也就是說如果list里面有 null 元素, 我們又想使用removeAll, 怎么辦?
首先我們使用
list.removeAll(Collections.singleton(null));

運行結果,沒問題:
接著我們也試著使用
list.removeAll(Arrays.asList(null));

運行結果,報錯,空指標例外:
其實是因為 Arrays.asList這個方法 , 請看原始碼:

再看new ArrayList的構造方法,也是不允許為空的:
PS: 但是這只是構造方法的規定,千萬別搞錯了 ,ArrayList是可以存盤 null 元素的 , add(null) 可沒有說不允許null元素傳入,
![]()
回到剛剛的話題, 那么我們運行沒有問題的 Collections.singleton(null) 怎么就沒報 空指標例外呢?
那是因為回傳的是Set, 而 Set的構造方法也是允許傳入null的 :

![]()
所以在使用removeAll的時候,想移除 null 元素, 其實只需要傳入的集合里面 是null 元素 就可以,也就是說,可以笨拙地寫成這樣,也是ok的 (了解原理就行,不是說非得這樣寫,因為后面還有更好的方法介紹):
從一開始的 移除 C元素, 到 現在更特殊一點的移除 null 元素 ,
到這里,似乎已經有了一個 了解和 了結, remove 和 removeAll使用起來應該是沒啥問題,
但是本篇文章還沒結束, 越扯越遠,
因為我想給大家 了解更多,不廢話繼續說,
removeIf
這個方法,是java JDK1.8 版本, Collection以及其子類 新引入的 ,
那既然是新引入的,肯定也是有原因了,肯定是更加好用更加能解決我們移除list里面的某元素的痛點了,
我們直接結合使用方式過一下這個方法吧:
![]()
移除list里面的 null 元素 :
list.removeIf(Objects::isNull);

運行結果:
再來,我們寫的更加通用一點,還是移除 null 元素:
list.removeIf( o -> null ==o );
運行結果,沒有問題:
我們移除的條件更多一點 ,把C元素 和 null 元素 和 ”“ 元素都移除了 :
list.removeIf( o -> null ==o || o.equals("C") || o.equals(""));

運行結果,完美執行:
removeIf 我個人比較喜歡,推薦,
最后再說個移除某個元素的方法吧, stream 流結合過濾器條件使用 :
其實跟上邊的removeIf差不多,只不過 stream流的filter 的用法是把 符合條件 的留下, 不符合條件的都去除 :
所以我們想把C元素 和 null 元素 和 ”“ 元素都移除了 ,要寫成:
List<String> listNew = list.stream().filter( o -> !(null == o || o.equals("C") || o.equals("")) ).collect(Collectors.toList());

執行結果:
最后再羅嗦一下:
removeIf 、stream+filter 方式, 不僅僅局限于 list 中使用 ,
只要父類是 Collection<E> ,都可以用(所以處理map的時候可以把里面的EntrySet取出來使用),
SET :
![]()
![]()
Map:
![]()
![]()
好吧,該篇暫且就到此吧,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/250635.html
標籤:其他
