解決在for循環(huán)中remove list報(bào)錯(cuò)越界的問(wèn)題
最近在搞一個(gè)購(gòu)物車的功能,里面有一個(gè)批量刪除的操作,采用的是ExpandableListView以及BaseExpandableListAdapter。視乎跟本篇無(wú)關(guān)緊要,主要是為了記錄一個(gè)java基礎(chǔ)。迭代器iterator的使用
一、錯(cuò)誤代碼(主要就是購(gòu)物車的批量刪除)
/**
* 刪除選中的
*/
public void delSelect() {
int groupSize;
if (mGropBeens != null) {
groupSize = mGropBeens.size();
} else {
return;
}
for (int i = 0; i < groupSize; i++) {
int childSize = mGropBeens.get(i).getChildBean().size();
for (int j = 0; j < childSize; j++) {
if (mGropBeens.get(i).getChildBean().get(j).isChecked()) {
DataSupport.deleteAll(ShopcartBean.class, "product_id=?", mGropBeens.get(i).getChildBean().get(j).getProduct_id());
mGropBeens.get(i).getChildBean().remove(j);
}
}
}
notifyDataSetChanged();
}
分析一、其實(shí)就是一個(gè)循環(huán)遍歷list進(jìn)行remove的操作,后來(lái)我分析了一下,錯(cuò)誤的很明顯,就是你remove了這個(gè)list以后,list的長(zhǎng)度就改變了,然后你繼續(xù)遍歷,就會(huì)報(bào)錯(cuò)。感覺躲不了啊。錯(cuò)誤有了,我覺得無(wú)法下手,后面既然remove不了,我就先刪除本地?cái)?shù)據(jù)庫(kù)的方式,然后遍歷對(duì)data進(jìn)行賦值。。。躲一下
/**
* 刪除選中的
*/
public void delSelect() {
int groupSize;
if (mGropBeens != null) {
groupSize = mGropBeens.size();
} else {
return;
}
for (int i = 0; i < groupSize; i++) {
int childSize = mGropBeens.get(i).getChildBean().size();
for (int j = 0; j < childSize; j++) {
if (mGropBeens.get(i).getChildBean().get(j).isChecked()) {
DataSupport.deleteAll(ShopcartBean.class, "product_id=?", mGropBeens.get(i).getChildBean().get(j).getProduct_id());
// mGropBeens.get(i).getChildBean().remove(j);
}
}
}
//刷新數(shù)據(jù)源
for (int i = 0; i < mGropBeens.size(); i++) {
mGropBeens.get(i).getChildBean().clear();
List<ShopcartBean> shopcartBeanlists = DataSupport.where("top_category_id=?", mGropBeens.get(i).getGroupId()).find(ShopcartBean.class);
mGropBeens.get(i).setChildBean(shopcartBeanlists);
}
notifyDataSetChanged();
}
分析二、寫了這樣以后還是感覺很不爽啊。明明我都循環(huán)了一遍知道要?jiǎng)h除這個(gè)對(duì)象了,還要等遍歷完,僅僅改變它的適配器的data。感覺不爽,隨意的百度了下,發(fā)現(xiàn)有專門的解決方案,就是迭代器iterator
二、爭(zhēng)取的打開方式
/**
* 刪除選中的
*/
public void delSelect() {
int groupSize;
if (mGropBeens != null) {
groupSize = mGropBeens.size();
} else {
return;
}
for (int i = 0; i < groupSize; i++) {
Iterator<ShopcartBean> iterator = mGropBeens.get(i).getChildBean().iterator();
while (iterator.hasNext()) {
ShopcartBean shopcartBean = iterator.next();
if (shopcartBean.isChecked()) {
DataSupport.deleteAll(ShopcartBean.class, "product_id=?", shopcartBean.getProduct_id());
iterator.remove();
}
}
}
notifyDataSetChanged();
}
分析三、發(fā)現(xiàn)這個(gè)玩意感覺還是很驚喜的,同時(shí)也感嘆自己基礎(chǔ)薄弱了。
一般循環(huán)for和foreach都需要先知道集合的類型,甚至是集合內(nèi)元素的類型,即需要訪問(wèn)內(nèi)部的成員;iterator是一個(gè)接口類型,他不關(guān)心集合或者數(shù)組的類型,而且他還能隨時(shí)修改和刪除集合的元素。他不包含任何有關(guān)他所遍歷的序列的類型信息,能夠?qū)⒈闅v序列的操作與序列底層的 結(jié)構(gòu)分離。
基本使用模式就是:
List<object> arr=xxx;//把你的list賦值過(guò)來(lái)
Iterator it = arr.iterator();
while(it.hasNext()){
object o =it.next();//當(dāng)前遍歷對(duì)象
iterator.remove();//刪除或修改等等
}
三、ok,先做記錄,后面繼續(xù)深入。have a nice day.這就是在一個(gè)list中刪除多個(gè)元素的正確解法。
補(bǔ)充知識(shí):詳解ArrayList在遍歷時(shí)remove元素所發(fā)生的并發(fā)修改異常的原因及解決方法
本文將以“在遍歷中刪除”為著手點(diǎn),在其基礎(chǔ)上進(jìn)行源碼分析及相關(guān)問(wèn)題解決。modCount的含義、迭代器所包含的方法、為什么會(huì)發(fā)生并發(fā)修改異常都將會(huì)在這篇文章中進(jìn)行說(shuō)明。
引入
這是一個(gè)并發(fā)修改異常的示例,它使用了迭代器iterator來(lái)獲取元素,同時(shí)使用ArrayList自身的remove方法移除元素(使用增強(qiáng)for循環(huán)去遍歷獲取元素亦會(huì)如此,增強(qiáng)for循環(huán)底層用的也是迭代器,enhanced for loop is nothing but a syntactic sugar over Iterator in Java)
public static void main(String[] args) {
//請(qǐng)動(dòng)手實(shí)踐運(yùn)行一下
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String str = iterator.next();
if (str.equals("a")) {
list.remove(str);
} else {
System.out.println(str);
}
}
}
原因分析
ArrayList內(nèi)部實(shí)現(xiàn)了迭代器Itr,如圖所示

通過(guò)迭代器獲取元素時(shí)(iterator.next())會(huì)進(jìn)行checkForComodification,源碼如下
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;//cursor向后挪一位
return (E) elementData[lastRet = i];//lastRet為當(dāng)前取出的元素所在索引,后面會(huì)用到
}
final void checkForComodification() {/***再看這里***/
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
modCount即此列表已被結(jié)構(gòu)修改的次數(shù)。 結(jié)構(gòu)修改是改變列表大小的那些修改(如增刪,注意列表大小是size而不是capacity),或以其他方式擾亂它,使得正在進(jìn)行的迭代可能產(chǎn)生不正確的結(jié)果的那些修改。
而expectedModCount會(huì)在迭代器被創(chuàng)建出來(lái)時(shí)初始化為modCount,源碼如下
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
//instance methods...
}
是不是發(fā)現(xiàn)什么端倪了呢?當(dāng)調(diào)用remove時(shí)(進(jìn)而調(diào)用fastRemove)即被視為結(jié)構(gòu)修改,因此modCount的值是會(huì)發(fā)生變化的,這樣當(dāng)程序再次通過(guò)iterator.next()獲取元素時(shí),通過(guò)checkForComodification方法發(fā)現(xiàn)modCount變化了,而expectedModCount 依然是初始化時(shí)的值,因此拋出ConcurrentModificationException。
讓我們來(lái)確認(rèn)一下我們的想法,remove方法及fastRemove方法的源碼如下
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);/***看這行調(diào)用了fastRemove***/
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;/***再看這行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
}
這樣我們就對(duì)ArrayList在遍歷時(shí)remove元素所發(fā)生的并發(fā)修改異常有了一個(gè)明確的了解。
迭代器Itr的補(bǔ)充說(shuō)明:size是數(shù)組大小,cursor是下一元素的索引(雖然是下一元素的索引,但數(shù)組開始索引是從0開始的,所以cursor默認(rèn)初始化為0)數(shù)組的最大索引一定是小于size的(size-1)索引=size還要取元素的話將會(huì)越界。
在說(shuō)明“如何在不發(fā)生異常的情況下刪除數(shù)據(jù)”前,先說(shuō)一下根據(jù)上述示例可能會(huì)產(chǎn)生的其他問(wèn)題(如不感興趣也可跳過(guò)0.0)。
刪除不規(guī)范時(shí)所產(chǎn)生的其他問(wèn)題
問(wèn)題1.雖然remove的不規(guī)范,但是程序依然能夠運(yùn)行,雖然不符合預(yù)期,但是沒有發(fā)生并發(fā)修改異常
如果我們刪除的不是a,而是d的話(最大為e),將會(huì)輸出a b c而不會(huì)發(fā)生并發(fā)修改異常,代碼如下
while (iterator.hasNext()) {
String str = (String) iterator.next();
if (str.equals("d")) {//將原先的a改為d
list.remove(str);
} else {
System.out.println(str);
}
}
原因分析:因?yàn)閯h除d時(shí)cursor由3變?yōu)?(從0起算),size由5變?yōu)?。因此hasNext返回true,并且循環(huán)結(jié)束,因此不會(huì)輸出e(循環(huán)結(jié)束也意味著不會(huì)通過(guò)next進(jìn)行checkForComodification,所以不會(huì)引發(fā)異常)
問(wèn)題2.看似不會(huì)發(fā)生并發(fā)修改異常,可實(shí)際卻發(fā)生了0.0
如果我們將要?jiǎng)h除的元素改為e,那么當(dāng)刪除e時(shí)cursor由4變?yōu)?,size由5變?yōu)?,5明明大于4了應(yīng)該不會(huì)有下一元素了,不會(huì)進(jìn)入循環(huán)通過(guò)next取元素了,可當(dāng)這么想著的時(shí)候,異常卻發(fā)生了,代碼如下:
while (iterator.hasNext()) {
String str = (String) iterator.next();
if (str.equals("e")) {
list.remove(str);
} else {
System.out.println(str);
}
}
原因分析:這是由于iterator.hasNext()的原理導(dǎo)致,點(diǎn)擊hasNext()查看源碼可發(fā)現(xiàn),hasNext并不是由cursor < size來(lái)實(shí)現(xiàn)的而是通過(guò)cursor != size來(lái)實(shí)現(xiàn)的,這樣程序?qū)⒃俅芜M(jìn)入循環(huán)取元素進(jìn)而發(fā)生并發(fā)修改異常
public boolean hasNext() {
return cursor != size;
}
如何在不報(bào)錯(cuò)的情況下將元素刪除?
1.通過(guò)iterator獲取元素的同時(shí)使用iterator的remove方法移除元素,代碼如下
while (iterator.hasNext()) {
String str = (String) iterator.next();
if (str.equals("a")) {
iterator.remove();
} else {
System.out.println(str);
}
}
通過(guò)Itr的remove源碼可以發(fā)現(xiàn)(如下),它在每次刪除的同時(shí)還會(huì)更新expectedModCount為當(dāng)前自增后的modCount,使得下次通過(guò)iterator.next()取元素時(shí)經(jīng)得住checkForComodification校驗(yàn)(試想一下如果沒有checkForComodification的話,程序?qū)⒗^續(xù)循環(huán)下去,cursor本指向的是當(dāng)前元素索引的下一位,但remove后數(shù)據(jù)將整體向前竄一位,從而導(dǎo)致cursor指向的索引位置對(duì)應(yīng)的數(shù)據(jù)發(fā)生了偏差,上述問(wèn)題2的情況時(shí)若沒有進(jìn)行checkForComodification則還會(huì)發(fā)生NoSuchElementException異常,詳見上述next源碼)。
lastRet的值為最新一次通過(guò)next獲取元素時(shí),那個(gè)元素所對(duì)應(yīng)的索引,這里通過(guò)將cursor = lastRet,從而把cursor的索引向前移動(dòng)了一位,繼而避免了取數(shù)據(jù)時(shí)的偏差(cursor 與 lastRet的關(guān)系詳見上面的next源碼)
在這里lastRet 會(huì)歸為-1(它所對(duì)應(yīng)的元素已經(jīng)被刪除了),這也是為什么不能連續(xù)調(diào)用兩次迭代器的remove方法的原因,若執(zhí)意如此,該方法將會(huì)拋出IllegalStateException的異常
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
2.通過(guò)list自身的get方法獲取元素的同時(shí)通過(guò)自身的remove方法移除元素,代碼如下
for (int i = 0;i<list.size();i++){
String s = list.get(i);
if ("a".equals(s)){
list.remove(i);//進(jìn)而調(diào)用fastRemove
i--;//相當(dāng)于cursor = lastRet;將返回的下一元素的索引 = 返回的最新元素的索引(當(dāng)前元素索引)
}else {
System.out.println(s);
}
}
該種情況下不會(huì)將expectedModCount修正為最新的modCount,同時(shí)也不會(huì)進(jìn)行checkForComodification的檢查,若此時(shí)刪除并不修正當(dāng)前索引的話,將會(huì)造成上述的數(shù)據(jù)偏差(遍歷條件中的list.size()保存為固定值或連續(xù)調(diào)用list.remove(i)次數(shù)過(guò)多還可以發(fā)生索引越界異常0.0)
注意,不能保證迭代器的快速失敗行為,因?yàn)橥ǔ?lái)說(shuō),在存在不同步的并發(fā)修改的情況下,不可能做出任何嚴(yán)格的保證??焖偈〉牡鲿?huì)盡最大努力拋出ConcurrentModificationException。因此,編寫依賴于此異常的程序的正確性是錯(cuò)誤的:迭代器的快速失敗行為應(yīng)僅用于檢測(cè)錯(cuò)誤。
add請(qǐng)參照remove的方式去查閱ArrayList中的內(nèi)部類ListItr
以上這篇解決在for循環(huán)中remove list報(bào)錯(cuò)越界的問(wèn)題就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
詳解Spring Aop實(shí)例之AspectJ注解配置
本篇文章主要介紹了詳解Spring Aop實(shí)例之AspectJ注解配置,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-04-04
關(guān)于SpringBoot整合Canal數(shù)據(jù)同步的問(wèn)題
大家都知道canal是阿里巴巴旗下的一款開源工具,純java開發(fā),支持mysql數(shù)據(jù)庫(kù),本文給大家介紹SpringBoot整合Canal數(shù)據(jù)同步的問(wèn)題,需要的朋友可以參考下2022-03-03
SpringBoot配置線程池的實(shí)現(xiàn)示例
本文主要介紹了SpringBoot配置線程池的實(shí)現(xiàn)示例,主要包括在Spring Boot中創(chuàng)建和配置線程池,包括設(shè)置線程池的大小、隊(duì)列容量、線程名稱等參數(shù),感興趣的可以了解一下2023-09-09
Linux下用java -jar運(yùn)行可執(zhí)行jar包的方法教程
這篇文章主要給大家介紹了在Linux下用java -jar運(yùn)行可執(zhí)行jar包的方法教程,文中介紹的非常詳細(xì),相信對(duì)大家的工作或者學(xué)習(xí)具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-05-05
springcloud feign調(diào)其他微服務(wù)時(shí)參數(shù)是對(duì)象的問(wèn)題
這篇文章主要介紹了springcloud feign調(diào)其他微服務(wù)時(shí)參數(shù)是對(duì)象的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
java中處理stream.filter()的實(shí)例代碼
stream()是Java 8中的一個(gè)函數(shù)式接口,用于處理數(shù)據(jù)流,它可以從一個(gè)數(shù)據(jù)源,如集合,數(shù)組等生成一個(gè)流,這篇文章主要給大家介紹了關(guān)于java中處理stream.filter()的相關(guān)資料,需要的朋友可以參考下2024-08-08

