JAVA中簡單的for迴圈竟有這麼多坑,但願你沒踩過
highlight: atelier-cave-light theme: channing-cyan
實際的業務專案開發中,大家應該對從給定的list中剔除不滿足條件的元素
這個操作不陌生吧?
很多同學可以立刻想出很多種實現的方式,但你想到的這些實現方式都是人畜無害
的嗎?很多看似正常的操作其實背後是個陷阱,很多新手可能稍不留神就會掉入其中。
倘若不幸踩中:
- 程式碼執行時直接拋異常報錯,這個算是不幸中的萬幸,至少可以及時發現並去解決
- 程式碼執行不報錯,但是業務邏輯莫名其妙的出現各種奇怪問題,這種就比較悲劇了,因為這個問題稍不留神的話,可能就會給後續業務埋下隱患。
那麼,到底有哪些實現方式呢?哪些實現方式可能會存在問題呢?這裡我們一起探討下。注意哦,這裡討論的可不是茴香豆的“茴”字有有種寫法的問題,而是很嚴肅很現實也很容易被忽略的技術問題。
假設需求場景:
給定一個使用者列表allUsers,需要從該列表中剔除隸屬部門為dev的人員,將剩餘的人員資訊返回
踩坑操作
foreach迴圈剔除方式
很多新手的第一想法就是for迴圈逐個判斷校驗下然後符合條件的剔除掉就行了嘛~ so easy...
1分鐘就把程式碼寫完了:
```java
public List
```
然後信心滿滿的點選了執行按鈕:
```
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 弄啥嘞?咋拋異常了?
一不留神就踩坑裡了,下面就一起分析下為啥會拋異常。
原因分析:
JAVA的foreach語法實際處理是基於迭代器Iterator進行實現的。
在迴圈開始時,會首先建立一個迭代例項,這個迭代例項的expectedModCount
賦值為集合的modCount
。而每當迭代器使⽤ hashNext()
/ next()
遍歷下⼀個元素之前,都會檢測 modCount
變數與expectedModCount
值是否相等,相等的話就返回遍歷;否則就丟擲異常ConcurrentModificationException
,終⽌遍歷。
如果在迴圈中新增或刪除元素,是直接呼叫集合的add()
,remove()
方法,導致了modCount
增加或減少,但這些方法不會修改迭代例項中的expectedModCount
,導致在迭代例項中expectedModCount
與 modCount
的值不相等,丟擲ConcurrentModificationException異常。
下標迴圈操作
嗯哼?既然foreach方式不行,那就用原始的下標迴圈的方式來搞,總不會報錯了吧?依舊很easy ...
```java
public List
```
程式碼一氣呵成,執行一下,看下處理後的輸出:
```
{id=2, name='李四', department='dev'} {id=3, name='王五', department='product'} {id=4, name='鐵柱', department='pm'}
```
果然,不報錯了,結果也輸出了,完美~
等等?這樣真的OK了嗎?
我們的程式碼邏輯裡面是判斷如果"dev".equals(department)
,但是輸出結果裡面,為啥還是有department=dev
這種本應被剔除掉的資料呢?
這裡如果是在真實業務專案中,開發階段不報錯,又沒有仔細去驗證結果的情況下,流到生產線上,就可能造成業務邏輯的異常。
接下來看下出現這個現象的具體原因。
原因分析:
我們知道,list中的元素與下標之間,其實並沒有強繫結關係,僅僅只是一個位置順序的對應關係,list中元素變更之後,其每個元素對應的下標都可能會變更,如下示意:
那麼,從List中刪除元素之後,List中被刪元素後面的所有元素下標都發生前移,但是for
迴圈的指標i
是始終往後累加的,再處理下一個的時候,就可能會有部分元素被漏掉沒有處理。
比如下圖的示意,i=0
時,判斷A元素需要刪除,則直接刪除;再迴圈時i=1
,此時因為list中元素位置前移,導致B元素變成了原來下標為0的位置,直接被漏掉了:
所以到這裡呢,也就可以知道為啥上面的程式碼執行後會出現漏網之魚啦~
正確方式
見識了上面2個坑操作之後,那正確妥當的操作方式應該是怎麼樣的呢?
迭代器方式
誒?沒搞錯吧?前面不是剛說過foreach方式也是使用的迭代器,但是其實是坑操作嗎?這裡怎麼又說迭代器模式是正確方式呢?
雖然都是基於迭代器,但是使用邏輯是不一樣的,看下程式碼:
```java
public List
```
執行結果:
```
{id=3, name='王五', department='product'} {id=4, name='鐵柱', department='pm'}
```
這次竟然直接執行成功了,且結果也是正確的。為啥呢?
在前面foreach
方式的時候,我們提過之所以會報錯的原因,是由於直接修改了原始list
資料而沒有同步讓Iterator
感知到,所以導致Iterator
操作前校驗失敗拋異常了。而此處的寫法中,直接呼叫迭代器中的remove()
方法,此操作會在呼叫集合的remove()
,add()
方法後,將expectedModCount
重新賦值為modCount
,所以在迭代器中增加、刪除元素是可以正常執行的。,所以這樣就不會出問題啦。
Lumbda表示式
言簡意賅,直接上程式碼:
```java
public List
```
Stream流操作
作為JAVA8開始加入的Stream,使得這種場景實現起來更加的優雅與易懂:
```java
public List
```
中間物件輔助方式
既然前面說了不能直接迴圈的時候執行移除操作,那就先搞個list物件將需要移除的元素暫存起來,最後一起剔除就行啦 ~
嗯,雖然有點挫,但是不得不承認,實際情況中,很多人都在用這個方法 —— 說的就是你,你是不是也曾這麼寫過?
```java
public List
```
或者:
```java
public List
```
回顧
好啦,關於JAVA中迴圈場景中對列表操作的相關內容我們就聊這麼多了~ 你有踩過上面的坑麼?你還有什麼更好的方式來實現嗎?歡迎一起討論交流~
我是悟道,聊技術、又不僅僅聊技術~
如果覺得有用,請點個關注,也可以關注下我的公眾號【架構悟道】,獲取更及時的更新。
期待與你一起探討,一起成長為更好的自己。
我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿。
- JAVA中使用最廣泛的本地快取?Ehcache的自信從何而來3 —— 解析Ehcache的各種叢集方案,本地快取如何變身分散式快取
- Redis快取何以一枝獨秀?——從百變應用場景與熱門面試題中感受下Redis的核心特性與使用注意點
- 聊一聊安全且正確使用快取的那些事 —— 關於快取可靠性、關乎資料一致性
- 聊一聊作為高併發系統基石之一的快取,會用很簡單,用好才是技術活
- JAVA基於CompletableFuture的流水線並行處理深度實踐,滿滿乾貨
- 講透JAVA Stream的collect用法與原理,遠比你想象的更強大
- 是時候優雅地和NullPointException說再見了
- 吃透JAVA的Stream流操作,多年實踐總結
- JAVA中計算兩個日期時間的差值竟然也有這麼多門道
- JAVA中簡單的for迴圈竟有這麼多坑,但願你沒踩過
- 我是如何將一個老系統的kafka消費者服務的效能提升近百倍的
- 搭建一個通用監控告警平臺,架構上需要有哪些設計
- 為什麼不建議使用自定義Object作為HashMap的key?
- 2022年中總結|桃李春風一杯酒,江湖夜雨十年燈
- Spring Data JPA系列3:JPA專案中核心場景與進階用法介紹
- Spring Data JPA系列2:SpringBoot整合JPA詳細教程,快速在專案中熟練使用JPA