解決在for循環(huán)中remove list報錯越界的問題
最近在搞一個購物車的功能,里面有一個批量刪除的操作,采用的是ExpandableListView以及BaseExpandableListAdapter。視乎跟本篇無關(guān)緊要,主要是為了記錄一個java基礎(chǔ)。迭代器iterator的使用
一、錯誤代碼(主要就是購物車的批量刪除)
/** * 刪除選中的 */ 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(); }
分析一、其實就是一個循環(huán)遍歷list進(jìn)行remove的操作,后來我分析了一下,錯誤的很明顯,就是你remove了這個list以后,list的長度就改變了,然后你繼續(xù)遍歷,就會報錯。感覺躲不了啊。錯誤有了,我覺得無法下手,后面既然remove不了,我就先刪除本地數(shù)據(jù)庫的方式,然后遍歷對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)了一遍知道要刪除這個對象了,還要等遍歷完,僅僅改變它的適配器的data。感覺不爽,隨意的百度了下,發(fā)現(xiàn)有專門的解決方案,就是迭代器iterator
二、爭取的打開方式
/** * 刪除選中的 */ 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)這個玩意感覺還是很驚喜的,同時也感嘆自己基礎(chǔ)薄弱了。
一般循環(huán)for和foreach都需要先知道集合的類型,甚至是集合內(nèi)元素的類型,即需要訪問內(nèi)部的成員;iterator是一個接口類型,他不關(guān)心集合或者數(shù)組的類型,而且他還能隨時修改和刪除集合的元素。他不包含任何有關(guān)他所遍歷的序列的類型信息,能夠?qū)⒈闅v序列的操作與序列底層的 結(jié)構(gòu)分離。
基本使用模式就是:
List<object> arr=xxx;//把你的list賦值過來 Iterator it = arr.iterator(); while(it.hasNext()){ object o =it.next();//當(dāng)前遍歷對象 iterator.remove();//刪除或修改等等 }
三、ok,先做記錄,后面繼續(xù)深入。have a nice day.這就是在一個list中刪除多個元素的正確解法。
補(bǔ)充知識:詳解ArrayList在遍歷時remove元素所發(fā)生的并發(fā)修改異常的原因及解決方法
本文將以“在遍歷中刪除”為著手點,在其基礎(chǔ)上進(jìn)行源碼分析及相關(guān)問題解決。modCount的含義、迭代器所包含的方法、為什么會發(fā)生并發(fā)修改異常都將會在這篇文章中進(jìn)行說明。
引入
這是一個并發(fā)修改異常的示例,它使用了迭代器iterator來獲取元素,同時使用ArrayList自身的remove方法移除元素(使用增強(qiáng)for循環(huán)去遍歷獲取元素亦會如此,增強(qiáng)for循環(huán)底層用的也是迭代器,enhanced for loop is nothing but a syntactic sugar over Iterator in Java)
public static void main(String[] args) { //請動手實踐運行一下 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)部實現(xiàn)了迭代器Itr,如圖所示
通過迭代器獲取元素時(iterator.next())會進(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)前取出的元素所在索引,后面會用到 } 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會在迭代器被創(chuàng)建出來時初始化為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時(進(jìn)而調(diào)用fastRemove)即被視為結(jié)構(gòu)修改,因此modCount的值是會發(fā)生變化的,這樣當(dāng)程序再次通過iterator.next()獲取元素時,通過checkForComodification方法發(fā)現(xiàn)modCount變化了,而expectedModCount 依然是初始化時的值,因此拋出ConcurrentModificationException。
讓我們來確認(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 }
這樣我們就對ArrayList在遍歷時remove元素所發(fā)生的并發(fā)修改異常有了一個明確的了解。
迭代器Itr的補(bǔ)充說明:size是數(shù)組大小,cursor是下一元素的索引(雖然是下一元素的索引,但數(shù)組開始索引是從0開始的,所以cursor默認(rèn)初始化為0)數(shù)組的最大索引一定是小于size的(size-1)索引=size還要取元素的話將會越界。
在說明“如何在不發(fā)生異常的情況下刪除數(shù)據(jù)”前,先說一下根據(jù)上述示例可能會產(chǎn)生的其他問題(如不感興趣也可跳過0.0)。
刪除不規(guī)范時所產(chǎn)生的其他問題
問題1.雖然remove的不規(guī)范,但是程序依然能夠運行,雖然不符合預(yù)期,但是沒有發(fā)生并發(fā)修改異常
如果我們刪除的不是a,而是d的話(最大為e),將會輸出a b c而不會發(fā)生并發(fā)修改異常,代碼如下
while (iterator.hasNext()) { String str = (String) iterator.next(); if (str.equals("d")) {//將原先的a改為d list.remove(str); } else { System.out.println(str); } }
原因分析:因為刪除d時cursor由3變?yōu)?(從0起算),size由5變?yōu)?。因此hasNext返回true,并且循環(huán)結(jié)束,因此不會輸出e(循環(huán)結(jié)束也意味著不會通過next進(jìn)行checkForComodification,所以不會引發(fā)異常)
問題2.看似不會發(fā)生并發(fā)修改異常,可實際卻發(fā)生了0.0
如果我們將要刪除的元素改為e,那么當(dāng)刪除e時cursor由4變?yōu)?,size由5變?yōu)?,5明明大于4了應(yīng)該不會有下一元素了,不會進(jìn)入循環(huán)通過next取元素了,可當(dāng)這么想著的時候,異常卻發(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)致,點擊hasNext()查看源碼可發(fā)現(xiàn),hasNext并不是由cursor < size來實現(xiàn)的而是通過cursor != size來實現(xiàn)的,這樣程序?qū)⒃俅芜M(jìn)入循環(huán)取元素進(jìn)而發(fā)生并發(fā)修改異常
public boolean hasNext() { return cursor != size; }
如何在不報錯的情況下將元素刪除?
1.通過iterator獲取元素的同時使用iterator的remove方法移除元素,代碼如下
while (iterator.hasNext()) { String str = (String) iterator.next(); if (str.equals("a")) { iterator.remove(); } else { System.out.println(str); } }
通過Itr的remove源碼可以發(fā)現(xiàn)(如下),它在每次刪除的同時還會更新expectedModCount為當(dāng)前自增后的modCount,使得下次通過iterator.next()取元素時經(jīng)得住checkForComodification校驗(試想一下如果沒有checkForComodification的話,程序?qū)⒗^續(xù)循環(huán)下去,cursor本指向的是當(dāng)前元素索引的下一位,但remove后數(shù)據(jù)將整體向前竄一位,從而導(dǎo)致cursor指向的索引位置對應(yīng)的數(shù)據(jù)發(fā)生了偏差,上述問題2的情況時若沒有進(jìn)行checkForComodification則還會發(fā)生NoSuchElementException異常,詳見上述next源碼)。
lastRet的值為最新一次通過next獲取元素時,那個元素所對應(yīng)的索引,這里通過將cursor = lastRet,從而把cursor的索引向前移動了一位,繼而避免了取數(shù)據(jù)時的偏差(cursor 與 lastRet的關(guān)系詳見上面的next源碼)
在這里lastRet 會歸為-1(它所對應(yīng)的元素已經(jīng)被刪除了),這也是為什么不能連續(xù)調(diào)用兩次迭代器的remove方法的原因,若執(zhí)意如此,該方法將會拋出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.通過list自身的get方法獲取元素的同時通過自身的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); } }
該種情況下不會將expectedModCount修正為最新的modCount,同時也不會進(jìn)行checkForComodification的檢查,若此時刪除并不修正當(dāng)前索引的話,將會造成上述的數(shù)據(jù)偏差(遍歷條件中的list.size()保存為固定值或連續(xù)調(diào)用list.remove(i)次數(shù)過多還可以發(fā)生索引越界異常0.0)
注意,不能保證迭代器的快速失敗行為,因為通常來說,在存在不同步的并發(fā)修改的情況下,不可能做出任何嚴(yán)格的保證??焖偈〉牡鲿M最大努力拋出ConcurrentModificationException。因此,編寫依賴于此異常的程序的正確性是錯誤的:迭代器的快速失敗行為應(yīng)僅用于檢測錯誤。
add請參照remove的方式去查閱ArrayList中的內(nèi)部類ListItr
以上這篇解決在for循環(huán)中remove list報錯越界的問題就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
關(guān)于SpringBoot整合Canal數(shù)據(jù)同步的問題
大家都知道canal是阿里巴巴旗下的一款開源工具,純java開發(fā),支持mysql數(shù)據(jù)庫,本文給大家介紹SpringBoot整合Canal數(shù)據(jù)同步的問題,需要的朋友可以參考下2022-03-03Linux下用java -jar運行可執(zhí)行jar包的方法教程
這篇文章主要給大家介紹了在Linux下用java -jar運行可執(zhí)行jar包的方法教程,文中介紹的非常詳細(xì),相信對大家的工作或者學(xué)習(xí)具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-05-05springcloud feign調(diào)其他微服務(wù)時參數(shù)是對象的問題
這篇文章主要介紹了springcloud feign調(diào)其他微服務(wù)時參數(shù)是對象的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03