詳解遍歷集合和遍歷集合時(shí)刪除集合元素

集合遍歷有多種方式,但各種方式執(zhí)行效率上稍有差別,遍歷集合時(shí)刪除元素處理不當(dāng)會(huì)有一些問(wèn)題,這里詳細(xì)匯總一下。

遍歷集合

遍歷集合元素的方式主要有以下幾種:

  • 使用一般的for循環(huán)遍歷集合
  • 使用for-each循環(huán)遍歷集合
  • 使用Iterator迭代器提供的hasNext、next方法遍歷集合
  • 使用Java 8 為Iterator接口提供的forEachRemaining默認(rèn)方法遍歷集合
  • 使用Java 8 為Iterable接口提供的forEach默認(rèn)方法遍歷集合
  • 使用Java 8 提供的流式API遍歷集合

這里以ArrayList為例來(lái)測(cè)試以上幾種方式。
先創(chuàng)建一個(gè)集合元素類。

public class Student {
    private int id;
    private String name;

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }

   省略getter和setter方法
}

再創(chuàng)建一個(gè)遍歷集合的測(cè)試類:

public class IterateCollectionTest {
    private static final int LIST_SIZE = 10000;
    private static final int ITERATE_TIMES = 1000;
    private static final int CONDITION_NUM = 2;

    public static void main(String[] args) {
        forIterate();
        forEachIterate();
        iteratorIterate();
        iteratorForEachRemainingMethodIterate();
        forEachMethodIterate();
        streamIterate();
    }

    /**
     * 使用一般的for循環(huán)遍歷集合元素
     */
    public static void forIterate() {
        List<Student> list = new ArrayList<>();
        for (int i = 1; i <= LIST_SIZE; i++) {
            list.add(new Student(i, "student" + i));
        }

        long millionSeconds = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) {
            Student student = list.get(i);
            if (student.getId() % CONDITION_NUM == 0) {
                for (int j = 0; j < ITERATE_TIMES; ) {
                    j++;
                }
            }
        }
        System.out.println("forIterate操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
    }

    /**
     * 使用for-each循環(huán)遍歷集合元素
     */
    public static void forEachIterate() {
        List<Student> list = new ArrayList<>();
        for (int i = 1; i <= LIST_SIZE; i++) {
            list.add(new Student(i, "student" + i));
        }

        long millionSeconds = System.currentTimeMillis();
        for (Student student : list) {
            if (student.getId() % CONDITION_NUM == 0) {
                for (int j = 0; j < ITERATE_TIMES; ) {
                    j++;
                }
            }
        }
        System.out.println("forEachIterate操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
    }

    /**
     * 使用Iterator迭代器來(lái)遍歷結(jié)合元素
     */
    public static void iteratorIterate() {
        List<Student> list = new ArrayList<>();
        for (int i = 1; i <= LIST_SIZE; i++) {
            list.add(new Student(i, "student" + i));
        }

        long millionSeconds = System.currentTimeMillis();
        Iterator<Student> iterator = list.iterator();
        while (iterator.hasNext()) {
            Student student = iterator.next();
            if (student.getId() % CONDITION_NUM == 0) {
                for (int j = 0; j < ITERATE_TIMES; ) {
                    j++;
                }
            }
        }
        System.out.println("iteratorIterate操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
    }

    /**
     * 使用Java 8 為Iterator接口提供的forEachRemaining默認(rèn)方法來(lái)遍歷集合元素
     * 該方法是使用Iterator的hasNext、next方法,以及函數(shù)式接口Consumer實(shí)現(xiàn)的
     * 該方法可依據(jù)指定的迭代順序(如果指定了的話)來(lái)遍歷處理集合元素,所以效率較低
     * 這里可以使用Java 8新增的Lambda表達(dá)式來(lái)簡(jiǎn)化編程
     */
    public static void iteratorForEachRemainingMethodIterate() {
        List<Student> list = new ArrayList<>();
        for (int i = 1; i <= LIST_SIZE; i++) {
            list.add(new Student(i, "student" + i));
        }

        long millionSeconds = System.currentTimeMillis();
        list.iterator().forEachRemaining(student -> {
            if (student.getId() % CONDITION_NUM == 0) {
                for (int j = 0; j < ITERATE_TIMES; ) {
                    j++;
                }
            }
        });
        System.out.println("iteratorForEachRemainingMethodIterate操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
    }

    /**
     * 使用Java 8 為Iterable接口提供的forEach默認(rèn)方法來(lái)遍歷集合元素
     * 該方法使用for-each循環(huán)來(lái)實(shí)現(xiàn)遍歷,在這幾種方法中速度最快
     * 這里可以使用Java 8新增的Lambda表達(dá)式來(lái)簡(jiǎn)化編程
     */
    public static void forEachMethodIterate() {
        List<Student> list = new ArrayList<>();
        for (int i = 1; i <= LIST_SIZE; i++) {
            list.add(new Student(i, "student" + i));
        }

        long millionSeconds = System.currentTimeMillis();
        list.forEach(student -> {
            if (student.getId() % CONDITION_NUM == 0) {
                for (int j = 0; j < ITERATE_TIMES; ) {
                    j++;
                }
            }
        });
        System.out.println("forEachMethodIterate操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
    }

    /**
     * 使用Java 8 提供的流式API來(lái)遍歷集合
     * 流式API將集合轉(zhuǎn)換成流,遍歷速度僅次于Iterable接口提供的forEach默認(rèn)方法。
     */
    public static void streamIterate() {
        List<Student> list = new ArrayList<>();
        for (int i = 1; i <= LIST_SIZE; i++) {
            list.add(new Student(i, "student" + i));
        }

        long millionSeconds = System.currentTimeMillis();
        list.stream().forEach(student -> {
            if (student.getId() % CONDITION_NUM == 0) {
                for (int j = 0; j < ITERATE_TIMES; ) {
                    j++;
                }
            }
        });
        System.out.println("streamIterate操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
    }
}

在我的i5-6500CPU電腦上多次測(cè)試取遍歷操作耗時(shí)的平均值,得出這幾種方法的遍歷速度從快到慢依次為:

forEachMethodIterate > streamIterate > iteratorIterate > forEachIterate > forIterate > iteratorForEachRemainingMethodIterate

所以如果遍歷一個(gè)集合中元素,建議優(yōu)先使用Java 8為Iterable接口提供的forEach默認(rèn)方法。如果你還未使用Java 8,則建議優(yōu)先使用Iterator接口的hasNex和next方法來(lái)實(shí)現(xiàn)遍歷

遍歷集合時(shí)動(dòng)態(tài)刪除集合中的元素

遍歷集合刪除集合元素的方式有以下幾種:

  • 使用一般的for循環(huán)遍歷刪除,同時(shí)手動(dòng)處理因刪除操作導(dǎo)致集合大小變化的問(wèn)題
  • 使用一般的for循環(huán)逆序遍歷刪除,不用手動(dòng)處理因刪除操作導(dǎo)致集合大小變化的問(wèn)題
  • 使用官方推薦的Iterator迭代器提供的Iterator.remove方法在遍歷集合時(shí)刪除集合元素
  • 使用Java 8新增的removeIf方法在遍歷集合時(shí)刪除集合元素
  • 使用Java 8提供的流式API來(lái)篩選元素,然后轉(zhuǎn)換成集合類型

這里以ArrayList為例來(lái)測(cè)試以上幾種方式。

public class RemoveElementInListTest {
    private static final int LIST_SIZE = 20000;
    private static final int CONDITION_NUM = 2;

    public static void main(String[] args) {
        // 以下三種方式都不能正常地遍歷刪除
        // forEachRemove();
        // forEachBreakRemove();
        // forRemove();

        // 以下幾種方式可以正常地遍歷刪除
        forRemoveNoSkipping();
        forReverseRemoveNoSkipping();
        iteratorRemove();
        ifRemove();
        streamRemove();
    }

    /**
     * 使用foreach遍歷刪除
     * 在第一次循環(huán)時(shí)刪除List中的元素刪除不會(huì)出現(xiàn)問(wèn)題,但繼續(xù)循環(huán)List時(shí)會(huì)報(bào)ConcurrentModificationException
     * 從打印的異常信息來(lái)看,forEach循環(huán)集合時(shí)使用了某種內(nèi)部索引器
     * 可以使用線程安全的CopyOnWriteArrayList來(lái)代替ArrayList
     * 但是當(dāng)List中元素很多時(shí)效率會(huì)大大折扣,還會(huì)造成資源浪費(fèi)
     */
    public static void forEachRemove() {
        List<Student> list = new ArrayList<>();
        for (int i = 1; i <= LIST_SIZE; i++) {
            list.add(new Student(i, "Student" + i));
        }

        for (Student student : list) {
            if (student.getId() % CONDITION_NUM == 0) {
                list.remove(student);
            }
        }
    }

    /**
     * 使用foreach循環(huán)對(duì)List進(jìn)行遍歷刪除,但刪除之后馬上就跳出的就不會(huì)出現(xiàn)異常
     * 但這種方式在需要?jiǎng)h除多個(gè)元素的情況下無(wú)法滿足要求
     */
    public static void forEachBreakRemove() {
        List<Student> list = new ArrayList<>();
        for (int i = 1; i <= LIST_SIZE; i++) {
            list.add(new Student(i, "Student" + i));
        }

        for (Student student : list) {
            if (student.getId() % CONDITION_NUM == 0) {
                list.remove(student);
                break;
            }
        }
    }

    /**
     * 一般的for循環(huán)遍歷有可能會(huì)遺漏某個(gè)元素,因?yàn)閯h除元素后List的size在變化,元素的索引也在變化
     * 比如你循環(huán)到第2個(gè)元素的時(shí)候你把它刪了,接下來(lái)你去訪問(wèn)第3個(gè)元素,實(shí)際上訪問(wèn)到的是原先的第4個(gè)元素
     * 當(dāng)訪問(wèn)的元素索引超過(guò)了當(dāng)前的List的size后還會(huì)出現(xiàn)數(shù)組越界的異常,當(dāng)然這里不會(huì)出現(xiàn)這種異常
     * 因?yàn)檫@里每遍歷一次都重新獲取一次當(dāng)前List的size
     */
    public static void forRemove() {
        List<Student> list = new ArrayList<>();
        for (int i = 1; i <= LIST_SIZE; i++) {
            list.add(new Student(i, "Student" + i));
        }

        for (int i = 0; i < list.size(); i++) {
            if (list.get(i).getId() % CONDITION_NUM == 0) {
                list.remove(i);
            }
        }
    }

    /**
     * 手動(dòng)處理一般的for循環(huán)遍歷時(shí)刪除而導(dǎo)致的索引變化就可以安全地刪除
     */
    public static void forRemoveNoSkipping() {
        List<Student> list = new ArrayList<>();
        for (int i = 1; i <= LIST_SIZE; i++) {
            list.add(new Student(i, "Student" + i));
        }

        long millionSeconds = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) {
            if (list.get(i).getId() % CONDITION_NUM == 0) {
                list.remove(i);
                // 刪除某個(gè)元素會(huì)導(dǎo)致list的size減1,手動(dòng)使遍歷位置后移一個(gè)位置
                // 這樣就不會(huì)漏掉被刪除元素后面的元素
                i--;
            }
        }
        System.out.println("forRemoveNoSkipping操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
    }

    /**
     * 使用反向的for循環(huán)遍歷時(shí)刪除就無(wú)需手動(dòng)處理索引變化的問(wèn)題
     * 而且因?yàn)閯h除操作而導(dǎo)致的元素移動(dòng)也比正向遍歷要少
     */
    public static void forReverseRemoveNoSkipping() {
        List<Student> list = new ArrayList<>();
        for (int i = 1; i <= LIST_SIZE; i++) {
            list.add(new Student(i, "Student" + i));
        }

        long millionSeconds = System.currentTimeMillis();
        for (int i = list.size() - 1; i >= 0; i--) {
            if (list.get(i).getId() % CONDITION_NUM == 0) {
                list.remove(i);
            }
        }
        System.out.println("forReverseRemoveNoSkipping操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
    }

    /**
     * 使用Iterator的方式也可以順利刪除和遍歷,不會(huì)有任何問(wèn)題,這才是刪除變量List中元素的正確方式
     */
    public static void iteratorRemove() {
        List<Student> list = new ArrayList<>();
        for (int i = 1; i <= LIST_SIZE; i++) {
            list.add(new Student(i, "Student" + i));
        }

        Iterator<Student> iterator = list.iterator();
        long millionSeconds = System.currentTimeMillis();
        while (iterator.hasNext()) {
            Student student = iterator.next();
            if (student.getId() % CONDITION_NUM == 0) {
                iterator.remove();
            }
        }
        System.out.println("iteratorRemove操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
    }

    /**
     * 也可以使用Java 8新增的removeIf方法在遍歷時(shí)刪除List中的元素,該方法也使用Iterator了,所以刪除是安全的
     */
    public static void ifRemove() {
        List<Student> list = new ArrayList<>();
        for (int i = 1; i <= LIST_SIZE; i++) {
            list.add(new Student(i, "Student" + i));
        }

        long millionSeconds = System.currentTimeMillis();
        list.removeIf(student -> student.getId() % CONDITION_NUM == 0);
        System.out.println("ifRemove操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
    }

    /**
     * 使用Java 8提供的流式API來(lái)篩選元素和轉(zhuǎn)換成集合類型
     * 從Java 8開始,使用流式API遍歷集合是首選的方式,這種方式用于遍歷非常快
     * 但由于創(chuàng)建了流,這種方式增大了空間開銷
     */
    public static void streamRemove() {
        List<Student> list = new ArrayList<>();
        for (int i = 1; i <= LIST_SIZE; i++) {
            list.add(new Student(i, "Student" + i));
        }

        long millionSeconds = System.currentTimeMillis();
        list.stream()
                .filter(student -> student.getId() % CONDITION_NUM == 0)
                .collect(Collectors.toList());
        System.out.println("streamRemove操作耗時(shí):" + (System.currentTimeMillis() - millionSeconds));
    }
}

在我的電腦上多次測(cè)試取耗時(shí)的平均值,得出這幾種方法的遍歷速度從快到慢依次為:

streamRemove > forReverseRemoveNoSkipping > iteratorRemove > forRemoveNoSkipping > ifRemove

其中,iteratorRemove和forRemoveNoSkipping的測(cè)試結(jié)果很接近,大家可以自行修改集合大小的常量親自測(cè)試,如有問(wèn)題歡迎反饋。

所以如果遍歷一個(gè)集合時(shí)刪除其中的元素,建議優(yōu)先使用Java 8提供的流式API來(lái)篩選集合元素。如果你還未使用Java 8,則建議優(yōu)先使用逆序的一般for循環(huán)來(lái)實(shí)現(xiàn)遍歷時(shí)刪除集合元素

許多初學(xué)者容易使用上面示例中的前三種方式來(lái)在遍歷集合時(shí)刪除集合元素,但是得不到正確的結(jié)果,原因已經(jīng)在這三種方法的注釋中說(shuō)明了。對(duì)于使用for-each循環(huán)時(shí)拋出ConcurrentModificationException異常的原因可通過(guò)查看ArrayList.remove()方法的源碼來(lái)探明。for-each循環(huán)List集合時(shí)使用了一個(gè)實(shí)現(xiàn)了Iterator接口的ArrayList內(nèi)部類對(duì)象來(lái)實(shí)現(xiàn)遍歷,該內(nèi)部類源碼如下:

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;

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        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;
            return (E) elementData[lastRet = i];
        }

        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();
            }
        }

使用for-each遍歷時(shí)調(diào)用該內(nèi)部類的next方法,進(jìn)而調(diào)用該方法中第一行的checkForComodification方法,ConcurrentModificationException異常就是在這個(gè)checkForComodification方法中拋出的:

final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

當(dāng)我們顯式調(diào)用remove方法來(lái)刪除集合中的元素時(shí)會(huì)修改modCount的值,使其與expectedModCount不一致:

public E remove(int index) {
        rangeCheck(index);
        checkForComodification();
        E result = l.remove(index+offset);
        this.modCount = l.modCount;
        size--;
        return result;
    }

官方教程也有說(shuō)在以下情況中可以使用Iterator來(lái)代替for-each循環(huán):

  • 刪除集合元素時(shí)。for-each循環(huán)使用隱藏了迭代器,所以遍歷刪除失敗。
  • 并行迭代多個(gè)集合對(duì)象時(shí)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容