關於面試題「ArrayList循環remove()要用Iterator」的研究

兩個月前我在參加一場面試的時候,被問到了ArrayList如何循環刪除元素,當時我回答用Iterator,當面試官問爲何要用Iterator而不用foreach時,我沒有答出來,現在又回想到了這個問題,我以爲應該把它搞一搞,因此我就寫了一個小的demo並結合閱讀源代碼來驗證了一下。java

下面是我驗證的ArrayList循環remove()的4種狀況,以及其結果(基於oracle jdk1.8):程序員

//List<Integer> list = new ArrayList<>();
//list.add(1);
//list.add(2);
//list.add(3);
//list.add(4);
//循環remove()的4種狀況的代碼片斷:

//#1
for (Integer integer : list) {
    list.remove(integer);
}

結果:
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
-----------------------------------------------------------------------------------

//#2
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()) {
    Integer integer = iterator.next();
    list.remove(integer);
}

結果:
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
-----------------------------------------------------------------------------------


//#3
for (int i = 0; i < list.size(); i++) {
    list.remove(i);
}
System.out.println(list);

結果:
[2, 4]
-----------------------------------------------------------------------------------

//#4
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
    iterator.next();
    iterator.remove();
}
System.out.println(list.size());

結果:(惟一一個獲得指望值的)
0複製代碼

能夠看出來這幾種狀況只有最後一種是獲得預期結果的,其餘的要麼異常要麼得不到預期結果,下面我們一個一個進行分析。面試

#1

//#1
for (Integer integer : list) {
    list.remove(integer);
}

結果:
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)複製代碼

經過異常棧,咱們能夠定位是在ArrayList的內部類ItrcheckForComodification方法中爆出了ConcurrentModificationException異常(關於這個異常是怎麼回事我們暫且不提)咱們打開ArrayList的源碼,定位到901行處:bash

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

這個爆出異常的方法實際上就作了一件事,檢查modCount != expectedModCount由於知足了這個條件,因此拋出了異常,繼續查看modCountexpectedModCount這兩個變量,發現modCount是繼承自AbstractList的一個屬性,這個屬性有一大段註釋併發

/**
 * The number of times this list has been <i>structurally modified</i>.
 * Structural modifications are those that change the size of the
 * list, or otherwise perturb it in such a fashion that iterations in
 * progress may yield incorrect results.
 *
 * <p>This field is used by the iterator and list iterator implementation
 * returned by the {@code iterator} and {@code listIterator} methods.
 * If the value of this field changes unexpectedly, the iterator (or list
 * iterator) will throw a {@code ConcurrentModificationException} in
 * response to the {@code next}, {@code remove}, {@code previous},
 * {@code set} or {@code add} operations.  This provides
 * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
 * the face of concurrent modification during iteration.
 *
 * <p><b>Use of this field by subclasses is optional.</b> If a subclass
 * wishes to provide fail-fast iterators (and list iterators), then it
 * merely has to increment this field in its {@code add(int, E)} and
 * {@code remove(int)} methods (and any other methods that it overrides
 * that result in structural modifications to the list).  A single call to
 * {@code add(int, E)} or {@code remove(int)} must add no more than
 * one to this field, or the iterators (and list iterators) will throw
 * bogus {@code ConcurrentModificationExceptions}.  If an implementation
 * does not wish to provide fail-fast iterators, this field may be
 * ignored.
 */
protected transient int modCount = 0;複製代碼

大體的意思是這個字段用於有fail-fast行爲的子集合類的,用來記錄集合被修改過的次數,咱們回到ArrayList能夠找到在add(E e)的調用鏈中的一個方法ensureExplicitCapacity(int minCapacity) 中會對modCount自增:oracle

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}複製代碼

咱們在初始化list時調用了4次add(E e)因此如今modCount的值爲4ide


再來找 expectedModCount:這個變量是定義在 ArrayListIterator的實現類 Itr中的,它默認被賦值爲 modCount


知道了這兩個變量是什麼了之後,咱們開始走查吧,在 Itr的相關方法中加好斷點(編譯器會將 foreach編譯爲使用 Iterator的方式,因此咱們看 Itr就能夠了),開始調試:

循環:
優化


在迭代的每次 next()時都會調用 checkForComodification()

list.remove()

ArrayListremove(Object o)中又調用了 fastRemove(index)


fastRemove(index)中對 modCount進行了自增,剛纔說過 modCount通過4次 add(E e)初始化後是 4因此 ++後如今是 5

繼續往下走,進入下次迭代:ui


又一次執行 next()next()調用 checkForComodification(),這時在上邊的過程當中 modCount因爲 fastRemove(index)的操做已經變成了 5expectedModCount則沒有人動,因此很快就知足了拋出異常的條件 modCount != expectedModCount(也就是前面提到的 fail-fast),程序退出。

#2

//#2
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()) {
    Integer integer = iterator.next();
    list.remove(integer);
}

結果:
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)複製代碼

其實這個#2和#1是同樣的,foreach會在編譯期被優化爲Iterator調用,因此看#1就好啦。this


#3

//#3
for (int i = 0; i < list.size(); i++) {
    list.remove(i);
}
System.out.println(list);

結果:
[2, 4]複製代碼

這種一本正經的胡說八道的狀況也許在寫代碼犯困的狀況下會出現... 不作文字解釋了,用println()來講明吧:

第0次循環開始
remove(0)前的list: [1, 2, 3, 4]
remove(0)前的list.size()=4
執行了remove(0)
remove(0)後的list.size()=3
remove(0)後的list: [2, 3, 4]
下一次循環的i=1
下一次循環的list.size()=3
第0次循環結束
是否還有條件進入下次循環?: true

第1次循環開始
remove(1)前的list: [2, 3, 4]
remove(1)前的list.size()=3
執行了remove(1)
remove(1)後的list.size()=2
remove(1)後的list: [2, 4]
下一次循環的i=2
下一次循環的list.size()=2
第1次循環結束
是否還有條件進入下次循環?: false


Process finished with exit code 0複製代碼

實際上ArrayListItr遊標最後一次返回值索引來解決了這種size越刪越小,可是要刪除元素的index愈來愈大的尷尬局面,這個將在#4裏說明。

#4

這個纔是正兒八經可以正確執行的方式,用了ArrayList中迭代器Itrremove()而不是用ArrayList自己的remove(),咱們調試一下吧看看到底經歷了什麼:

迭代:

Itr初始化:遊標 cursor = 0; 最後一次返回值索引 lastRet = -1; 指望修改次數 expectedModCount = modCount = 4;


迭代的 hasNext():檢查遊標是否已經到達當前list的 size,若是沒有則說明能夠繼續迭代:



迭代的 next()checkForComodification() 此時 expectedModCountmodCount是相等的,不會拋出 ConcurrentModificationException,而後取到遊標(第一次迭代遊標是 0)對應的list的元素,再將遊標+1,也就是遊標後移指向下一個元素,而後將遊標原值 0賦給最後一次返回值索引,也就是最後一次返回的是索引 0對應的元素

iterator.remove():一樣checkForComodification()而後調用ArrayListremove(lastRet)刪除最後返回的元素,刪除後modCount會自增

刪除完成後,將遊標賦值成最後一次返回值索引,其實也就是將遊標回退了一格回到了上一次的位置,而後將最後一次返回值索引從新設置爲了初始值-1,最後expectedModCount又從新賦值爲了上一步過程完成後新的modCount


由上兩個步驟能夠看出來,雖然list的 size每次 remove()都會 -1,可是因爲每次 remove()都會將遊標回退,而後將最後一次返回值索引重置,因此實際上沒回 remove()的都是當前集合的第 0個元素,就不會出現#3中 size越刪越小,而要刪除元素的索引愈來愈大的狀況了,同時因爲在 remove()過程當中 expectedModCountmodCount始終經過賦值保持相等,因此也不會出現 fail-fast拋出異常的狀況了。

以上是我經過走查源碼的方式對面試題「ArrayList循環remove()要用Iterator」作的一點研究,沒考慮併發場景,這篇文章寫了大概3個多小時,寫完這篇文章辦公室就剩我一我的了,我也該回去了,今天1024程序員節,你們節日快樂!


2017.10.25更新#1

感謝@llearn的提醒,#3也能夠用用巧妙的方式來獲得正確的結果的(再面試的時候,我以爲能夠和麪試官說不必定要用Iterator了,感謝@llearn

//#3 我以爲能夠這樣
for (int i = 0; i < list.size(); ) {
list.remove(0);
}
System.out.println(list);

2017.10.25更新#2

感謝@ChinLong的提醒,提供了另外一種不用Iterator的方法,也就是倒着循環(這種方案我寫完文章時也想到了,但沒有本身印證到demo上),感謝@ChinLong

然道就沒有人和我一下喜歡倒着刪的.聽別人說倒着迭代速度稍微快一點???for (int i = list.size() -1; i >= 0; i-- ) { list.remove(i);}System.out.println(list);

相關文章
相關標籤/搜索