List在遍歷時刪除元素的問題
背景:業務中經常會涉及遍歷list時對集合進行插入或者刪除操作
一、 錯誤方式
先看看下面幾段代碼,1是foreach的方式去遍歷list並刪除元素,2是用迭代器的方式遍歷list並刪除元素,3是下標遍歷
1. foreach
public void testDel(){
List<Integer> list = Lists.newArrayList();
for(int i=1;i<=5;i++){
list.add(i);
}
for(Integer ele : list){
if(ele == 3)
list.remove(ele);
}
}
2. Iterator
public void testDel(){
List<Integer> list = Lists.newArrayList();
for(int i=1;i<=5;i++){
list.add(i);
}
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
Integer integer = iterator.next();
if(integer==3)
list.remove(integer);
}
}
以上兩段代碼的執行結果都是 java.util.ConcurrentModificationException。
在使用ArrayList時,當嘗試用foreach或者Iterator遍歷集合時進行刪除或者插入元素的操作時,會拋出這樣的異常:java.util.ConcurrentModificationException
關於這個異常的原因,看了很多文章,基本上解釋如下:ArrayList的父類AbstarctList中有一個域modCount,每次對集合進行修改(增添、刪除元素)時modCount都會+1。
而foreach的實現原理就是迭代器Iterator,在這裏,迭代ArrayList的Iterator中有一個變量expectedModCount,該變量會初始化和modCount相等,但當對集合進行插入,刪除操作,modCount會改變,就會造成expectedModCount!=modCount,此時就會拋出java.util.ConcurrentModificationException異常,是在checkForComodification方法中,代碼如下:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
那麼,以上兩種方式都不用呢,用下標遍歷呢?看如下代碼:
3 . 下標遍歷
public void testDel() {
List<Integer> list = Lists.newArrayList();
list.add(1);
list.add(2);
list.add(2);
list.add(2);
list.add(2);
for(int i=0;i<list.size();i++){
if(list.get(i)==2){
list.remove(i);
}
}
}
//結果: list = [1,2,2]
因爲下標是固定死的自增,但list的大小在隨着刪除元素不停的減小,並且後面的元素往前移了1位,所以後面的元素遍歷不到。在i=3時,由於刪了2個元素,size=3,所以循環直接結束。因此這種方式也是不符合預期的。
二、解決方法
但業務中經常涉及遍歷時刪除或插入操作。所以如何安全的操作ArrayList呢,解決方法如下!
1.使用迭代器的remove方法
在使用迭代器遍歷時,可使用迭代器的remove方法,因爲Iterator的remove方法中 有如下的操作:
expectedModCount = modCount;
所以避免了ConcurrentModificationException的異常。代碼如下:
public void testDel() {
List<Integer> list = Lists.newArrayList();
list.add(1);
list.add(2);
list.add(2);
list.add(2);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer integer = iterator.next();
if (integer == 2)
iterator.remove();
}
}
執行後結果: list = [1]
其實在阿里巴巴Java開發手冊中原話:不要在 foreach 循環裏進行元素的 remove/add 操作。remove 元素請使用 Iterator方式,如果併發操作,需要對 Iterator 對象加鎖。
2.倒序遍歷刪除
public void testDel() {
List<Integer> list = Lists.newArrayList();
list.add(1);
list.add(2);
list.add(2);
list.add(2);
list.add(2);
for(int i=list.size()-1;i>=0;i--){
if(list.get(i)==2){
list.remove(i);
}
}
}
結果: list = [1]
可見,也達到了預期的效果。因爲每次刪除一個元素,list大小-1,但是倒序,循環條件爲i>=0,所以list的size改變並沒有對遍歷造成影響,且元素的前移也不會對倒序遍歷有影響。
所以在對list或者hashmap遍歷時候進行元素刪增操作時,一定要驗證下,我開始想當然的以爲OK,調試才發現前面幾種方式是有問題的,不是異常,就是效果沒達到預期。到線上會出大問題。所以整理了下,推薦最後兩種方式!