解決在for循環(huán)中remove list報(bào)錯(cuò)越界的問題
最近在搞一個(gè)購(gòu)物車的功能,里面有一個(gè)批量刪除的操作,采用的是ExpandableListView以及BaseExpandableListAdapter。視乎跟本篇無關(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的操作,后來我分析了一下,錯(cuò)誤的很明顯,就是你remove了這個(gè)list以后,list的長(zhǎng)度就改變了,然后你繼續(xù)遍歷,就會(huì)報(bào)錯(cuò)。感覺躲不了啊。錯(cuò)誤有了,我覺得無法下手,后面既然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)元素的類型,即需要訪問內(nèi)部的成員;iterator是一個(gè)接口類型,他不關(guān)心集合或者數(shù)組的類型,而且他還能隨時(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)前遍歷對(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)問題解決。modCount的含義、迭代器所包含的方法、為什么會(huì)發(fā)生并發(fā)修改異常都將會(huì)在這篇文章中進(jìn)行說明。
引入
這是一個(gè)并發(fā)修改異常的示例,它使用了迭代器iterator來獲取元素,同時(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,如圖所示
通過迭代器獲取元素時(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)建出來時(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)程序再次通過iterator.next()獲取元素時(shí),通過checkForComodification方法發(fā)現(xiàn)modCount變化了,而expectedModCount 依然是初始化時(shí)的值,因此拋出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 }
這樣我們就對(duì)ArrayList在遍歷時(shí)remove元素所發(fā)生的并發(fā)修改異常有了一個(gè)明確的了解。
迭代器Itr的補(bǔ)充說明:size是數(shù)組大小,cursor是下一元素的索引(雖然是下一元素的索引,但數(shù)組開始索引是從0開始的,所以cursor默認(rèn)初始化為0)數(shù)組的最大索引一定是小于size的(size-1)索引=size還要取元素的話將會(huì)越界。
在說明“如何在不發(fā)生異常的情況下刪除數(shù)據(jù)”前,先說一下根據(jù)上述示例可能會(huì)產(chǎn)生的其他問題(如不感興趣也可跳過0.0)。
刪除不規(guī)范時(shí)所產(chǎ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ì)通過next進(jìn)行checkForComodification,所以不會(huì)引發(fā)異常)
問題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)通過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來實(shí)現(xiàn)的而是通過cursor != size來實(shí)現(xiàn)的,這樣程序?qū)⒃俅芜M(jìn)入循環(huán)取元素進(jìn)而發(fā)生并發(fā)修改異常
public boolean hasNext() { return cursor != size; }
如何在不報(bào)錯(cuò)的情況下將元素刪除?
1.通過iterator獲取元素的同時(shí)使用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)(如下),它在每次刪除的同時(shí)還會(huì)更新expectedModCount為當(dāng)前自增后的modCount,使得下次通過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ā)生了偏差,上述問題2的情況時(shí)若沒有進(jìn)行checkForComodification則還會(huì)發(fā)生NoSuchElementException異常,詳見上述next源碼)。
lastRet的值為最新一次通過next獲取元素時(shí),那個(gè)元素所對(duì)應(yīng)的索引,這里通過將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.通過list自身的get方法獲取元素的同時(shí)通過自身的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ù)過多還可以發(fā)生索引越界異常0.0)
注意,不能保證迭代器的快速失敗行為,因?yàn)橥ǔ碚f,在存在不同步的并發(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ò)越界的問題就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
詳解Spring Aop實(shí)例之AspectJ注解配置
本篇文章主要介紹了詳解Spring Aop實(shí)例之AspectJ注解配置,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-04-04關(guān)于SpringBoot整合Canal數(shù)據(jù)同步的問題
大家都知道canal是阿里巴巴旗下的一款開源工具,純java開發(fā),支持mysql數(shù)據(jù)庫(kù),本文給大家介紹SpringBoot整合Canal數(shù)據(jù)同步的問題,需要的朋友可以參考下2022-03-03SpringBoot配置線程池的實(shí)現(xiàn)示例
本文主要介紹了SpringBoot配置線程池的實(shí)現(xiàn)示例,主要包括在Spring Boot中創(chuàng)建和配置線程池,包括設(shè)置線程池的大小、隊(duì)列容量、線程名稱等參數(shù),感興趣的可以了解一下2023-09-09Linux下用java -jar運(yùn)行可執(zhí)行jar包的方法教程
這篇文章主要給大家介紹了在Linux下用java -jar運(yùn)行可執(zhí)行jar包的方法教程,文中介紹的非常詳細(xì),相信對(duì)大家的工作或者學(xué)習(xí)具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。2017-05-05springcloud feign調(diào)其他微服務(wù)時(shí)參數(shù)是對(duì)象的問題
這篇文章主要介紹了springcloud feign調(diào)其他微服務(wù)時(shí)參數(shù)是對(duì)象的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03java中處理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