兩個月前我在參加一場面試的時候,被問到了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
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
的內部類Itr
的checkForComodification
方法中爆出了ConcurrentModificationException
異常(關於這個異常是怎麼回事我們暫且不提)咱們打開ArrayList
的源碼,定位到901行處:bash
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}複製代碼
這個爆出異常的方法實際上就作了一件事,檢查modCount != expectedModCount
由於知足了這個條件,因此拋出了異常,繼續查看modCount
和expectedModCount
這兩個變量,發現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
的值爲4
ide
expectedModCount
:這個變量是定義在
ArrayList
的
Iterator
的實現類
Itr
中的,它默認被賦值爲
modCount
Itr
的相關方法中加好斷點(編譯器會將
foreach
編譯爲使用
Iterator
的方式,因此咱們看
Itr
就能夠了),開始調試:
循環:
優化
next()
時都會調用
checkForComodification()
list.remove()
:
ArrayList
的
remove(Object o)
中又調用了
fastRemove(index)
:
fastRemove(index)
中對
modCount
進行了自增,剛纔說過
modCount
通過4次
add(E e)
初始化後是
4
因此
++
後如今是
5
:
繼續往下走,進入下次迭代:ui
next()
,
next()
調用
checkForComodification()
,這時在上邊的過程當中
modCount
因爲
fastRemove(index)
的操做已經變成了
5
而
expectedModCount
則沒有人動,因此很快就知足了拋出異常的條件
modCount != expectedModCount
(也就是前面提到的
fail-fast
),程序退出。
//#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
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複製代碼
實際上ArrayList
中Itr
用遊標
和最後一次返回值索引
來解決了這種size
越刪越小,可是要刪除元素的index愈來愈大的尷尬局面,這個將在#4裏說明。
這個纔是正兒八經可以正確執行的方式,用了ArrayList
中迭代器Itr
的remove()
而不是用ArrayList
自己的remove()
,咱們調試一下吧看看到底經歷了什麼:
迭代:
Itr
初始化:遊標 cursor = 0; 最後一次返回值索引 lastRet = -1; 指望修改次數 expectedModCount = modCount = 4;
hasNext()
:檢查遊標是否已經到達當前list的
size
,若是沒有則說明能夠繼續迭代:
next()
:
checkForComodification()
此時
expectedModCount
和
modCount
是相等的,不會拋出
ConcurrentModificationException
,而後取到遊標(第一次迭代遊標是
0
)對應的list的元素,再將遊標+1,也就是遊標後移指向下一個元素,而後將遊標原值
0
賦給最後一次返回值索引,也就是最後一次返回的是索引
0
對應的元素
iterator.remove()
:一樣checkForComodification()
而後調用ArrayList
的remove(lastRet)
刪除最後返回的元素,刪除後modCount
會自增
刪除完成後,將遊標賦值成最後一次返回值索引,其實也就是將遊標回退了一格回到了上一次的位置,而後將最後一次返回值索引從新設置爲了初始值-1
,最後expectedModCount
又從新賦值爲了上一步過程完成後新的modCount
size
每次
remove()
都會
-1
,可是因爲每次
remove()
都會將遊標回退,而後將最後一次返回值索引重置,因此實際上沒回
remove()
的都是當前集合的第
0
個元素,就不會出現#3中
size
越刪越小,而要刪除元素的索引愈來愈大的狀況了,同時因爲在
remove()
過程當中
expectedModCount
和
modCount
始終經過賦值保持相等,因此也不會出現
fail-fast
拋出異常的狀況了。
以上是我經過走查源碼的方式對面試題「ArrayList循環remove()要用Iterator」作的一點研究,沒考慮併發場景,這篇文章寫了大概3個多小時,寫完這篇文章辦公室就剩我一我的了,我也該回去了,今天1024程序員節,你們節日快樂!
感謝@llearn
的提醒,#3也能夠用用巧妙的方式來獲得正確的結果的(再面試的時候,我以爲能夠和麪試官說不必定要用Iterator
了,感謝@llearn
:
//#3 我以爲能夠這樣
for (int i = 0; i < list.size(); ) {
list.remove(0);
}
System.out.println(list);
感謝@ChinLong
的提醒,提供了另外一種不用Iterator
的方法,也就是倒着循環(這種方案我寫完文章時也想到了,但沒有本身印證到demo上),感謝@ChinLong
:
然道就沒有人和我一下喜歡倒着刪的.聽別人說倒着迭代速度稍微快一點???for (int i = list.size() -1; i >= 0; i-- ) { list.remove(i);}System.out.println(list);