2024年8月8日 星期四

如何處理集合型態物件的 ConcurrentModificationException


不知道大家有沒有遇到過,開發功能中若使用集合型態物件(無論是宣告為區域變數或屬性),常利用迴圈(while+Iterator或forEach)逐個取得集合元素。若此時需要在迴圈中移除(最後一個以外的)某個元素,並進入下一個元素(next( )方法)時,會發生ConcurrentModificationException而造成異常結束。

下列程式,來示範這個情況。這裡定義的MySet類別中有定義一個private TreeSet型態的集合屬性,並提供了getter以符合屬性封裝的設計原則。在建構式中也對dateSet加入了「從今天往前最近的星期日開始」一週的日期(LocalDate)物件,程式如下:

import java.time.LocalDate;
import java.util.Set;
import java.util.TreeSet;

public class MySet {
    private Set<LocalDate> dateSet = new TreeSet<>();

    public MySet() {
        LocalDate today = LocalDate.now();
        int i= -today.getDayOfWeek().getValue();

        for (int j=i;j<(7+i);j++) { //加入從今天往前最近星期日起一週日期
            dateSet.add(today.plusDays(j));
        }
    }

    /**
     * @param keyDate 移除dateSet中指定的日期物件
     */
    public void remove(LocalDate keyDate) {
        dateSet.remove(keyDate);
    }

    /**
     * @return 直接回傳dateSet屬性參考(這是不正確的設計)
     */
    public Set<LocalDate> getDateSet() {
        return dateSet;   //直接回傳該set物件參考
    }
    
    @Override
    public String toString() {
        return "MySet [dateSet=" + dateSet + "]";
    }
}

在TestMySet的main方法中示範的程式會發生集合型態物件特有的例外:
java.util.ConcurrentModificationException
程式如下:

package patty.mod15.test;

import java.time.LocalDate;
import java.util.Iterator;
import patty.mod15.entity.MySet;

public class TestMySet {
    public static void main(String[] args) {
        MySet mySet = new MySet();
        System.out.println(mySet);  //列出mySet的內容
        
        LocalDate today = LocalDate.now();
        Iterator<LocalDate> mySetIterator=mySet.getDateSet().iterator();
        while(mySetIterator.hasNext()) {
            LocalDate theDate = mySetIterator.next();  //這裡是第15行
            if (today.isAfter(theDate)) {  //將小於今天的日期移除
                mySet.remove(theDate);
            }
        }
        
        System.out.println(mySet);  //列出mySet的內容
    }
}

執行結果如下圖,在main程式的第15行mySetIterator.next()方法發生下列錯誤:


而發生錯誤的主因就是:在用Iterator指位器逐個取得集合元素時,直接將元素從集合中移除,會造成指位器往下的順序錯亂。就好比把一串珠珠從中間剪斷一個,後面的珠串也會就斷掉而無法繼續處理。

因此正確簡單的修改方式,是取得原來集合物件的複本,也就是建立新的集合元件來複製原來的集合內容,讓複本集合先抓住每個元素,藉由複本的Iterator來取得元素,再到正本(來源集合)remove想要移除的元素,就不會造成Iterator指位器的順序錯亂了。

所以是在MySet程式中修改dateSet屬性的getter內容,如下圖第32行的程式:


而TestMySet類別main方法中看來錯誤的第15行程式,則完全不用修改,如下圖:


直接執行就會有正確的結果了,如下圖:


您可在下列課程中了解更多技巧喔!

0 意見:

張貼留言