JAVA中簡(jiǎn)單的for循環(huán)異常踩坑
引言
實(shí)際的業(yè)務(wù)項(xiàng)目開發(fā)中,大家應(yīng)該對(duì)從給定的list中剔除不滿足條件的元素這個(gè)操作不陌生吧?
很多同學(xué)可以立刻想出很多種實(shí)現(xiàn)的方式,但你想到的這些實(shí)現(xiàn)方式都是人畜無害的嗎?很多看似正常的操作其實(shí)背后是個(gè)陷阱,很多新手可能稍不留神就會(huì)掉入其中。
倘若不幸踩中:
- 代碼運(yùn)行時(shí)直接拋異常報(bào)錯(cuò),這個(gè)算是不幸中的萬幸,至少可以及時(shí)發(fā)現(xiàn)并去解決
- 代碼運(yùn)行不報(bào)錯(cuò),但是業(yè)務(wù)邏輯莫名其妙的出現(xiàn)各種奇怪問題,這種就比較悲劇了,因?yàn)檫@個(gè)問題稍不留神的話,可能就會(huì)給后續(xù)業(yè)務(wù)埋下隱患。
那么,到底有哪些實(shí)現(xiàn)方式呢?哪些實(shí)現(xiàn)方式可能會(huì)存在問題呢?這里我們一起探討下。注意哦,這里討論的可不是茴香豆的“茴”字有有種寫法的問題,而是很嚴(yán)肅很現(xiàn)實(shí)也很容易被忽略的技術(shù)問題。
假設(shè)需求場(chǎng)景:
給定一個(gè)用戶列表allUsers,需要從該列表中剔除隸屬部門為dev的人員,將剩余的人員信息返回
踩坑操作
foreach循環(huán)剔除方式
很多新手的第一想法就是for循環(huán)逐個(gè)判斷校驗(yàn)下然后符合條件的剔除掉就行了嘛~ so easy...
1分鐘就把代碼寫完了:
public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) { for (UserDetail user : allUsers) { // 判斷部門如果屬于dev,則直接剔除 if ("dev".equals(user.getDepartment())) { allUsers.remove(user); } } // 返回剩余的用戶數(shù)據(jù) return allUsers; }
然后信心滿滿的點(diǎn)擊了執(zhí)行按鈕:
java.util.ConcurrentModificationException: null at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909) at java.util.ArrayList$Itr.next(ArrayList.java:859) at com.veezean.demo4.UserService.filterAllDevDeptUsers(UserService.java:13) at com.veezean.demo4.Main.main(Main.java:26)
誒? what are you 弄啥嘞?咋拋異常了?
一不留神就踩坑里了,下面就一起分析下為啥會(huì)拋異常。
原因分析:
JAVA的foreach語法實(shí)際處理是基于迭代器Iterator進(jìn)行實(shí)現(xiàn)的。
在循環(huán)開始時(shí),會(huì)首先創(chuàng)建一個(gè)迭代實(shí)例,這個(gè)迭代實(shí)例的expectedModCount 賦值為集合的modCount。而每當(dāng)?shù)魇? hashNext() / next() 遍歷下?個(gè)元素之前,都會(huì)檢測(cè) modCount 變量與expectedModCount 值是否相等,相等的話就返回遍歷;否則就拋出異常ConcurrentModificationException,終?遍歷。
如果在循環(huán)中添加或刪除元素,是直接調(diào)用集合的add(),remove()方法,導(dǎo)致了modCount增加或減少,但這些方法不會(huì)修改迭代實(shí)例中的expectedModCount,導(dǎo)致在迭代實(shí)例中expectedModCount與 modCount的值不相等,拋出ConcurrentModificationException異常。
下標(biāo)循環(huán)操作
嗯哼?既然foreach方式不行,那就用原始的下標(biāo)循環(huán)的方式來搞,總不會(huì)報(bào)錯(cuò)了吧?依舊很easy ...
public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) { for (int i = 0; i < allUsers.size(); i++) { // 判斷部門如果屬于dev,則直接剔除 if ("dev".equals(allUsers.get(i).getDepartment())) { allUsers.remove(i); } } // 返回剩余的用戶數(shù)據(jù) return allUsers; }
代碼一氣呵成,執(zhí)行一下,看下處理后的輸出:
{id=2, name='李四', department='dev'}
{id=3, name='王五', department='product'}
{id=4, name='鐵柱', department='pm'}
果然,不報(bào)錯(cuò)了,結(jié)果也輸出了,完美~
等等?這樣真的OK了嗎?
我們的代碼邏輯里面是判斷如果"dev".equals(department),但是輸出結(jié)果里面,為啥還是有department=dev這種本應(yīng)被剔除掉的數(shù)據(jù)呢?
這里如果是在真實(shí)業(yè)務(wù)項(xiàng)目中,開發(fā)階段不報(bào)錯(cuò),又沒有仔細(xì)去驗(yàn)證結(jié)果的情況下,流到生產(chǎn)線上,就可能造成業(yè)務(wù)邏輯的異常。
接下來看下出現(xiàn)這個(gè)現(xiàn)象的具體原因。
原因分析:
我們知道,list中的元素與下標(biāo)之間,其實(shí)并沒有強(qiáng)綁定關(guān)系,僅僅只是一個(gè)位置順序的對(duì)應(yīng)關(guān)系,list中元素變更之后,其每個(gè)元素對(duì)應(yīng)的下標(biāo)都可能會(huì)變更,如下示意:
那么,從List中刪除元素之后,List中被刪元素后面的所有元素下標(biāo)都發(fā)生前移,但是for循環(huán)的指針i是始終往后累加的,再處理下一個(gè)的時(shí)候,就可能會(huì)有部分元素被漏掉沒有處理。
比如下圖的示意,i=0時(shí),判斷A元素需要?jiǎng)h除,則直接刪除;再循環(huán)時(shí)i=1,此時(shí)因?yàn)閘ist中元素位置前移,導(dǎo)致B元素變成了原來下標(biāo)為0的位置,直接被漏掉了:
所以到這里呢,也就可以知道為啥上面的代碼執(zhí)行后會(huì)出現(xiàn)漏網(wǎng)之魚啦~
正確方式
見識(shí)了上面2個(gè)坑操作之后,那正確妥當(dāng)?shù)牟僮鞣绞綉?yīng)該是怎么樣的呢?
迭代器方式
誒?沒搞錯(cuò)吧?前面不是剛說過foreach方式也是使用的迭代器,但是其實(shí)是坑操作嗎?這里怎么又說迭代器模式是正確方式呢?
雖然都是基于迭代器,但是使用邏輯是不一樣的,看下代碼:
public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) { Iterator<UserDetail> iterator = allUsers.iterator(); while (iterator.hasNext()) { // 判斷部門如果屬于dev,則直接剔除 if ("dev".equals(iterator.next().getDepartment())) { // 這是重點(diǎn),此處操作的是Iterator,而不是list iterator.remove(); } } // 返回剩余的用戶數(shù)據(jù) return allUsers; }
執(zhí)行結(jié)果:
{id=3, name='王五', department='product'}
{id=4, name='鐵柱', department='pm'}
這次竟然直接執(zhí)行成功了,且結(jié)果也是正確的。為啥呢?
在前面foreach方式的時(shí)候,我們提過之所以會(huì)報(bào)錯(cuò)的原因,是由于直接修改了原始list數(shù)據(jù)而沒有同步讓Iterator感知到,所以導(dǎo)致Iterator操作前校驗(yàn)失敗拋異常了。
而此處的寫法中,直接調(diào)用迭代器中的remove()方法,此操作會(huì)在調(diào)用集合的remove(),add()方法后,將expectedModCount重新賦值為modCount,所以在迭代器中增加、刪除元素是可以正常運(yùn)行的。,所以這樣就不會(huì)出問題啦。
Lambda表達(dá)式
言簡(jiǎn)意賅,直接上代碼:
public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) { allUsers.removeIf(user -> "dev".equals(user.getDepartment())); return allUsers; }
Stream流操作
作為JAVA8開始加入的Stream,使得這種場(chǎng)景實(shí)現(xiàn)起來更加的優(yōu)雅與易懂:
public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) { return allUsers.stream() .filter(user -> !"dev".equals(user.getDepartment())) .collect(Collectors.toList()); }
中間對(duì)象輔助方式
既然前面說了不能直接循環(huán)的時(shí)候執(zhí)行移除操作,那就先搞個(gè)list對(duì)象將需要移除的元素暫存起來,最后一起剔除就行啦 ~
嗯,雖然有點(diǎn)挫,但是不得不承認(rèn),實(shí)際情況中,很多人都在用這個(gè)方法 —— 說的就是你,你是不是也曾這么寫過?
public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) { List<UserDetail> needRemoveUsers = new ArrayList<>(); for (UserDetail user : allUsers) { if ("dev".equals(user.getDepartment())) { needRemoveUsers.add(user); } } allUsers.removeAll(needRemoveUsers); return allUsers; }
或者:
public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) { List<UserDetail> resultUsers = new ArrayList<>(); for (UserDetail user : allUsers) { if (!"dev".equals(user.getDepartment())) { resultUsers.add(user); } } return resultUsers; }
以上就是JAVA中簡(jiǎn)單的for循環(huán)異常踩坑的詳細(xì)內(nèi)容,更多關(guān)于JAVA for循環(huán)異常的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java守護(hù)線程實(shí)例詳解_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
在Java中有兩類線程:User Thread(用戶線程)、Daemon Thread(守護(hù)線程) 。下面通過本文給大家分享java守護(hù)線程實(shí)例詳解,需要的朋友參考下吧2017-06-06微信公眾帳號(hào)開發(fā)-自定義菜單的創(chuàng)建及菜單事件響應(yīng)的實(shí)例
本篇文章主要介紹了微信公眾帳號(hào)開發(fā)-自定義菜單的創(chuàng)建及菜單事件響應(yīng)的實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2016-12-12Springcloud-nacos實(shí)現(xiàn)配置和注冊(cè)中心的方法
這篇文章主要介紹了Springcloud-nacos實(shí)現(xiàn)配置和注冊(cè)中心的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07Spring Security OAuth2 token權(quán)限隔離實(shí)例解析
這篇文章主要介紹了Spring Security OAuth2 token權(quán)限隔離實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11Springboot集成SSE實(shí)現(xiàn)單工通信消息推送流程詳解
SSE簡(jiǎn)單的來說就是服務(wù)器主動(dòng)向前端推送數(shù)據(jù)的一種技術(shù),它是單向的,也就是說前端是不能向服務(wù)器發(fā)送數(shù)據(jù)的。SSE適用于消息推送,監(jiān)控等只需要服務(wù)器推送數(shù)據(jù)的場(chǎng)景中,下面是使用Spring Boot來實(shí)現(xiàn)一個(gè)簡(jiǎn)單的模擬向前端推動(dòng)進(jìn)度數(shù)據(jù),前端頁面接受后展示進(jìn)度條2022-11-11