首頁 > Java > java教程 > 如何避免JAVA中簡單的for迴圈出現異常?

如何避免JAVA中簡單的for迴圈出現異常?

WBOY
發布: 2023-04-26 12:58:07
轉載
1113 人瀏覽過

引言

實際的業務專案開發中,大家應該對從給定的list中剔除不滿足條件的元素這個操作不陌生吧?

很多同學可以立刻想出很多實現的方式,但你想到的這些實現方式都是人畜無害的嗎?很多看似正常的操作其實背後是個陷阱,很多新手可能稍不留神就會掉進去。

倘若不幸踩中:

  • 程式碼運行時直接拋異常報錯,這個算是不幸中的萬幸,至少可以及時發現並去解決

  • #程式碼運作不報錯,但是業務邏輯莫名其妙的出現各種奇怪問題,這種就比較悲劇了,因為這個問題稍不留神的話,可能就會給後續業務埋下隱患。

那麼,到底有哪些實作方式呢?哪些實現方式可能會有問題呢?這裡我們一起探討下。注意哦,這裡討論的可不是茴香豆的「壕」字有種寫法的問題,而是很嚴肅很現實也很容易被忽略的技術問題。

假設需求場景:

給定一個使用者清單allUsers,需要從這個清單中剔除隸屬部門為dev的人員,將剩餘的人員資訊回傳

踩坑操作

foreach循環剔除方式

很多新手的第一想法就是for循環逐一判斷校驗下然後符合條件的剔除掉就行了嘛~ so easy...

1分鐘就把程式碼寫完了:

public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) {
    for (UserDetail user : allUsers) {
        // 判断部门如果属于dev,则直接剔除
        if ("dev".equals(user.getDepartment())) {
            allUsers.remove(user);
        }
    }
    // 返回剩余的用户数据
    return allUsers;
}
登入後複製

然後信心滿滿的點擊了執行按鈕:

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異常。

如何避免JAVA中簡單的for迴圈出現異常?

下標循環運算

嗯哼?既然foreach方式不行,那就用原始的下標迴圈的方式來搞,總不會報錯了吧?依舊很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);
        }
    }
    // 返回剩余的用户数据
    return allUsers;
}
登入後複製

程式碼一氣呵成,執行一下,看下處理後的輸出:

{id=2, name='李四', department=' dev'}
{id=3, name='王五', department='product'}
{id=4, name='鐵柱', department='pm'}

#果然,不報錯了,結果也輸出了,完美~

等等?這樣真的OK了嗎?

我們的程式碼邏輯裡面是判斷如果"dev".equals(department),但是輸出結果裡面,為啥還是有department=dev這種本應被剔除掉的資料呢?

這裡如果是在真實業務專案中,開發階段不報錯,又沒有仔細去驗證結果的情況下,流到生產線上,就可能造成業務邏輯的異常。

接下來看下出現這個現象的具體原因。

原因分析:

我們知道,list中的元素與下標之間,其實並沒有強綁定關係,僅僅只是一個位置順序的對應關係,list中元素變更之後,其每個元素對應的下標都可能會變更,如下示意:

如何避免JAVA中簡單的for迴圈出現異常?

那麼,從List中刪除元素之後,List中被刪元素後面的所有元素下標都發生前移,但是for迴圈的指標i是總是往後累加的,再處理下一個的時候,就可能會有部分元素被漏掉沒有處理。

例如下圖的示意,i=0時,判斷A元素需要刪除,則直接刪除;再循環時i=1,此時因為list中元素位置前移,導致B元素變成了原來下標為0的位置,直接被漏掉了:

如何避免JAVA中簡單的for迴圈出現異常?

所以到這裡呢,也就可以知道為啥上面的程式碼執行後會出現漏網之魚啦~

正確方式

見識了上面2個坑操作之後,那正確妥當的操作方式應該是怎麼樣的呢?

迭代器方式

誒?沒錯吧?前面不是剛剛說foreach方式也是使用的迭代器,但其實是坑操作嗎?這裡怎麼又說迭代器模式才是正確方式呢?

虽然都是基于迭代器,但是使用逻辑是不一样的,看下代码:

public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) {
    Iterator<UserDetail> iterator = allUsers.iterator();
    while (iterator.hasNext()) {
        // 判断部门如果属于dev,则直接剔除
        if ("dev".equals(iterator.next().getDepartment())) {
            // 这是重点,此处操作的是Iterator,而不是list
            iterator.remove();
        }
    }
    // 返回剩余的用户数据
    return allUsers;
}
登入後複製

执行结果:

{id=3, name='王五', department='product'}
{id=4, name='铁柱', department='pm'}

这次竟然直接执行成功了,且结果也是正确的。为啥呢?

在前面foreach方式的时候,我们提过之所以会报错的原因,是由于直接修改了原始list数据而没有同步让Iterator感知到,所以导致Iterator操作前校验失败抛异常了。

而此处的写法中,直接调用迭代器中的remove()方法,此操作会在调用集合的remove(),add()方法后,将expectedModCount重新赋值为modCount,所以在迭代器中增加、删除元素是可以正常运行的。,所以这样就不会出问题啦。

如何避免JAVA中簡單的for迴圈出現異常?

Lambda表达式

言简意赅,直接上代码:

public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) {
    allUsers.removeIf(user -> "dev".equals(user.getDepartment()));
    return allUsers;
}
登入後複製

Stream流操作

作为JAVA8开始加入的Stream,使得这种场景实现起来更加的优雅与易懂:

public List<UserDetail> filterAllDevDeptUsers(List<UserDetail> allUsers) {
    return allUsers.stream()
            .filter(user -> !"dev".equals(user.getDepartment()))
            .collect(Collectors.toList());
}
登入後複製

中间对象辅助方式

既然前面说了不能直接循环的时候执行移除操作,那就先搞个list对象将需要移除的元素暂存起来,最后一起剔除就行啦 ~

嗯,虽然有点挫,但是不得不承认,实际情况中,很多人都在用这个方法 —— 说的就是你,你是不是也曾这么写过?

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中簡單的for迴圈出現異常?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:yisu.com
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板