關於java中ArrayList的快速失敗機制的漏洞——使用迭代器循環時刪除倒數第二個元素不會報錯

1、問題描述java

話很少說,先上代碼:安全

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
     list.add("第零個"); list.add(
"第一個"); list.add("第二個"); list.add("第三個"); list.add("第四個"); for (String str : list) { if (str.equals("第三個")) { System.out.println("刪除:" + str); list.remove(str); } } System.out.println(list); }

知道快速失敗機制的可能都會說,不能在foreach循環裏用集合直接刪除,應該使用iterator的remove()方法,不然會報錯:java.util.ConcurrentModificationException函數

可是這個代碼的真實輸出結果倒是:this

並無報錯,這是爲何呢?spa

 

2、基礎知識線程

java的foreach 和 快速失敗機制仍是先簡單介紹一下:指針

foreach過程:code

Java在經過foreach遍歷集合列表時,會先爲列表建立對應的迭代器,並經過調用迭代器的hasNext()函數判斷是否含下一個元素,如有則調用iterator.next()獲取繼續遍歷,沒有則結束遍歷。blog

快速失敗機制:接口

由於非線程安全,迭代器的next()方法調用時會判斷modCount==expectedModCount,不然拋出ConcurrentModIficationException。modCount只要元素數量變化(add,remove)就+1,而集合和表的add和remove方法卻不會更新expectedModCount,只有迭代器remove會重置expectedModCount=modCount,並將cursor往前一位。因此在使用迭代器循環的時候,只能使用迭代器的修改。

 

3、分析

因此由上面的foreach介紹咱們能夠知道上面的foreach循環代碼能夠寫成以下形式:

        for (Iterator iterator = list.iterator(); iterator.hasNext();) {
            String str = (String) iterator.next();
            if (str.equals("第三個")) {
                System.out.println("刪除:" + str);
                list.remove(str);
            }
        }

重點就在於 iterator.next() 這裏,咱們看看next()的源碼:【此處的迭代器是ArrayList的私有類,實現了迭代器接口Iterator,重寫了各類方法】

 1         public E next() {
 2             checkForComodification();
 3             try {
 4                 int i = cursor;
 5                 E next = get(i);
 6                 lastRet = i;
 7                 cursor = i + 1;
 8                 return next;
 9             } catch (IndexOutOfBoundsException e) {
10                 checkForComodification();
11                 throw new NoSuchElementException();
12             }
13         }

注意到第7行!,也就是說,cursor最開始是 i,調用next()後就將第 i 個元素返回,而後變成下一個元素的下標了,因此在遍歷到倒數第二個元素的時候cursor已經爲最後一個元素的下標(size-1)了

注意了!在調用集合或者.remove(int)的方法時,並不會對cursor進行改變,【具體操做:將元素刪除後,調用System.arraycopy讓後面的的元素往前移動一位,並將最後一個元素位釋放】,而本程序中此時的size變成了原來的size-1=4,而cursor仍是原來的size-1=4,兩者相等!,再看看斷定hasNext():

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

此時cursor==size()==4,程序覺得此時已經遍歷完畢,因此根本不會進入循環中,也就是說根本不會進入到next()方法裏,也就不會有checkForComodification() 的判斷。

 

 4、驗證

咱們在程序中foreach循環的第一句獲取str後面加入一個打印,  System.out.println(str); ,

這樣每次進入foreach循環就會打印1,其餘不變,咱們再來運行一次,結果以下:

 

顯然,第四個元素沒有被遍歷到,分析正確,那假如使用iterator.remove()呢?

那咱們再來看看iterator.remove()的源碼,【此處的iterator是ArrayList的私有類,實現了迭代器接口Iterator,重寫了各類方法】

 1         public void remove() {
 2             if (lastRet < 0)
 3                 throw new IllegalStateException();
 4             checkForComodification();
 5 
 6             try {
 7                 AbstractList.this.remove(lastRet);
 8                 if (lastRet < cursor)
 9                     cursor--;
10                 lastRet = -1;
11                 expectedModCount = modCount;
12             } catch (IndexOutOfBoundsException e) {
13                 throw new ConcurrentModificationException();
14             }
15         }

看第7行,內部其實也是調用的所屬list的remove(int)方法,可是不一樣的地方要注意了:

第9行:將cursor--,也就是說在刪除當前元素後,他又把移動後的指針放回了當前元素下標,因此繼續循環不會跳過當前元素位的新值;

第11行:expectedModCount = modCount; 更新expectedModCount,使兩者相同,在繼續循環中不會被checkForComodification()檢測出報錯。

 

5、結論

1. 每次foreach循環開始時next()方法會使cursor變爲下一個元素下標;

2. ArrayList的remove()方法執行完後,下一個元素移動到被刪除元素位置上,由1可知,cursor此時指向原來被刪除元素的下一個的下一個元素所在位置,此時繼續foreach循環,被刪除元素的下一個元素不會被遍歷到

3. checkForComodification()方法用來實現快速失敗機制的判斷,此方法在iterator.next()方法中,必須在進入foreach循環後纔會被調用;

4. 由2,當ArrayList的remove()方法在foreach刪除倒數第二個元素時,繼續foreach循環,倒數第一個元素會被跳過,從而退出循環,聯合3可知,在刪除倒數第二個元素後,並不會進入快速失敗機制的判斷。

5. iterator.remove()方法會在刪除和移動元素後將cursor放回正確的位置,不會致使元素跳過,而且更新expectedModCount,是一個安全的選擇。

相關文章
相關標籤/搜索