foreach遍歷list集合刪除某些元素一定會(huì)報(bào)錯(cuò)嗎?
先上一段代碼:
List list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
for (String item : list) {
if (item.equals("3")) {
System.out.println(item);
list.remove(item);
}
}
System.out.println(list.size());
控制臺(tái)報(bào)錯(cuò):java.util.ConcurrentModificationException。
這是怎么回事,然后去看了看這個(gè)異常,才發(fā)現(xiàn)自己果然還是太年輕啊。
我們都知道增加for循環(huán)即foreach循環(huán)其實(shí)就是根據(jù)list對(duì)象創(chuàng)建一個(gè)iterator迭代對(duì)象,用這個(gè)迭代對(duì)象來(lái)遍歷list,相當(dāng)于list對(duì)象中元素的遍歷托管給了iterator,如果要對(duì)list進(jìn)行增刪操作,都必須經(jīng)過(guò)iterator。
每次foreach循環(huán)時(shí)都有以下兩個(gè)操作:
- iterator.hasNext(); //判讀是否有下個(gè)元素
- item = iterator.next(); //下個(gè)元素是什么,并把它賦給item。
首先,我們來(lái)看看這個(gè)異常信息是什么。
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification(); // 此處報(bào)錯(cuò)
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];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
可以看到是進(jìn)入checkForComodification()方法的時(shí)候報(bào)錯(cuò)了,也就是說(shuō)modCount != expectedModCount。具體的原因是:以foreach方式遍歷元素的時(shí)候,會(huì)生成iterator,然后使用iterator遍歷。在生成iterator的時(shí)候,會(huì)保存一個(gè)expectedModCount參數(shù),這個(gè)是生成iterator的時(shí)候List中修改元素的次數(shù)。如果你在遍歷過(guò)程中刪除元素,List中modCount就會(huì)變化,如果這個(gè)modCount和exceptedModCount不一致,就會(huì)拋出異常,這個(gè)是為了安全考慮。
看看list的remove源碼:
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
看,并沒(méi)有對(duì)expectedModCount進(jìn)行任何修改,導(dǎo)致expectedModCount和modCount不一致,拋出異常。
但是,遍歷list刪除元素使用Iterator則不會(huì)報(bào)錯(cuò),如下:
Iterator it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("3")) {
it.remove();
}
}
看看Iterator的remove()方法的源碼,是對(duì)expectedModCount重新做了賦值處理的,如下:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount; // 處理expectedModCount
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
這樣的話保持expectedModCount = modCount相等,就不會(huì)報(bào)出錯(cuò)了。
是不是foreach所有的list刪除操作都會(huì)報(bào)出這個(gè)錯(cuò)呢?
其實(shí)不一定。如果刪除的元素是倒數(shù)第二個(gè)數(shù)的話,其實(shí)是不會(huì)報(bào)錯(cuò)的。為什么呢,來(lái)一起看看。
之前說(shuō)了foreach循環(huán)會(huì)走兩個(gè)方法hasNext() 和next()。如果不想報(bào)錯(cuò)的話,只要不進(jìn)next()方法就好啦,看看hasNext()的方法。
public boolean hasNext() {
return cursor != size;
}
那么就要求hasNext()的方法返回false了,即cursor == size。其中cursor是Itr類(Iterator子類)中的一個(gè)字段,用來(lái)保存當(dāng)前iterator的位置信息,從0開(kāi)始。cursor本身就是游標(biāo)的意思,在數(shù)據(jù)庫(kù)的操作中用的比較多。只要curosr不等于size就認(rèn)為存在元素。由于Itr是ArrayList的內(nèi)部類,因此直接調(diào)用了ArrayList的size字段,所以這個(gè)字段的值是動(dòng)態(tài)變化的,既然是動(dòng)態(tài)變化的可能就會(huì)有問(wèn)題出現(xiàn)了。
我們以上面的代碼為例,當(dāng)?shù)降箶?shù)第二個(gè)數(shù)據(jù)也就是“4”的時(shí)候,cursor是4,然后調(diào)用刪除操作,此時(shí)size由5變成了4,當(dāng)再調(diào)用hasNext判斷的時(shí)候,cursor==size,就會(huì)調(diào)用后面的操作直接退出循環(huán)了。我們可以在上面的代碼添加一行代碼查看效果:
for (String item : list) {
System.out.println(item);
if (item.equals("4")) {
list.remove(item);
}
}
輸出是:
1
2
3
4
這樣的話就可以看到執(zhí)行到hasNext()方法就退出了,也就不會(huì)走后面的異常了。
由此可以得出,用foreach刪除list元素的時(shí)候只有倒數(shù)第二個(gè)元素刪除不會(huì)報(bào)錯(cuò),其他都會(huì)報(bào)錯(cuò),所以刪除list元素時(shí)一定要用Iterator。
參考文獻(xiàn):
https://blog.csdn.net/bimuyulaila/article/details/52088124