ArrayList中Iterator的研究學習


       最近去某公司面試,被問到了一個簡單的問題,ArrayList中要刪除一些元素應該怎么操作?答曰:"使用Iterator迭代器遍歷,判斷之后再用迭代器提供的remove()方法將判斷為true的元素刪掉",問:“為什么要選擇這個方法?”答曰:“迭代器采用的是fail-fast機制,foreach循環內部調用了迭代器,刪除元素破壞了集合的結構,所以會報錯”,追問:“為什么不能用for循環呢?沒使用迭代器也應該可行啊?為什么迭代器又是可以的呢?” 答曰:“因為for循環刪掉元素后會錯位,至於迭代器是怎么解決這個問題的, 還真不知道...” 

       作為一名菜鳥,招架不了這么連珠炮般的提問,於是回去讀了一遍關於Iterator的源碼,簡要的解析一番。本次學習過程大概是圍繞這幾個問題展開的:

 

       1、Iterator和Iterable的關系

         Iterator是java中的一個接口,定義了一系列方法。諸如hasNext()判定、next()取下一元素的值和remove()刪除當前元素值。

         Iterable是java中的一個接口,定義了一個Iterator的實現類作為成員變量,也提供了for-each這個語法糖。
            
        正如它們的英文意思,Iterator規定了作為一個迭代器應該具有的方法,而Iterable則是規定了如果一個類可以被迭代,不僅要自己實現屬於自己的迭代器,而且還附送一個for-each語法糖給程序員作為快捷手段。

   

         2、fail-fast機制   

  • 什么是fail-fast?
      The iterators returned by this class's {@link #iterator() iterator} and
 * {@link #listIterator(int) listIterator} methods are <em>fail-fast</em>:</a>
 * if the list is structurally modified at any time after the iterator is
 * created, in any way except through the iterator's own
 * {@link ListIterator#remove() remove} or
 * {@link ListIterator#add(Object) add} methods, the iterator will throw a
 * {@link ConcurrentModificationException}.  Thus, in the face of
 * concurrent modification, the iterator fails quickly and cleanly, rather
 * than risking arbitrary, non-deterministic behavior at an undetermined
 * time in the future.

 

       這個官方解釋已經非常明白了:為防止並發(可以理解為一切Iterator體系之外的方法)修改,迭代器一旦被調出來之后,任何除了Iterator類中定義的方法以外的方法(包括集合的clear、remove和add方法),對集合元素進行修改,都屬於結構化的改變(無論是別的線程還是自己的線程),迭代器就會拋出ConcurrentModificationException的錯誤。這里的結構改變,確切的說指的是集合的size發生變化。(注意:並非是elementData這個數組的length發生改變,因為數組中的元素完全可以為null,length不變,而集合中這個元素刪掉了,size就得減1)。
 
  • 為什么會報錯?

                  在這里就不貼代碼了,直接上結果:

    • ArrayList提供了一個成員變量modCount,這個變量表示整個list被結構化修改的次數,初始值為0。
    • ArrayList的remove、add、clear方法執行的時候,內部會執行modCount++
    • ArrayList中的迭代器中提供一個成員變量expectedModCount,初始值為modCount,記錄在迭代器被創建時的集合結構改變的次數。
    • 迭代器的next、remove方法在執行之前都會調用checkForModification方法判定expectedModCount與modCount之間是否相等,如果不相等,則拋出concurrentModificationException

                  這就可以解釋為什么for-each不能在內部使用集合的刪除方法了——因為它底層調用了Iterator的next方法進行遍歷。

             

          3、為什么Iterator.remove()不會出現錯位?

                  這也是Iterator設計的精髓所在。       

        private class Itr implements Iterator<E> {
        int cursor;       // 即將取出的元素的角標
        int lastRet = -1; // 上一次取出的元素的角標

int expectedModCount = modCount; // Iterator創建時的改動初始值 public boolean hasNext() { return cursor != size; //判斷是否還有下一位元素存在,該方法要實現持續遍歷判斷,必須要配合next()方法 } @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; // 更新cursor的值為下一輪判定做准備 return (E) elementData[lastRet = i]; // 給lastRet賦值,否則remove()方法報錯 } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; //對cursor進行修正 lastRet = -1; //remove()方法不能連續執行,因為lastRet不合法,只能先執行next() expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
  • 將迭代器設計成內部類

         因為迭代器需要實現接口中操作對象的方法,而不同的對象其底層數據結構不同,因此迭代器的實現類需要根據對象的特點進行設計,也就是說迭代器只有在相應的對象中才有用,不如就在其內部定義得了。

  • 三個方法的全家桶
    • hasNext()方法
                    取cursor與size進行對比,用來判斷是否還能繼續遍歷下去。
            當cursor=size的時候,后續方法不管是next()遍歷取值還是remove()刪除都因為超界不能繼續,所以此時判定依然沒任何意義。
    • next()方法
             這個方法很簡單,最終返回的即是elementData[lastRet]這個元素,即把當前的cursor對應的數據取出。值得注意的是
      • 在每次該方法執行的邏輯尾部cursor=cursor+1——使得hasNext()的遍歷得以繼續下去
      • 方法中引入了中間變量i,提前將cursor的值賦給i后,返值時又將i賦給lastRet——使得remove()順利實現
    • remove()方法
      • 首先判定lastRet<0是否成立,如果成立則報錯。Iterator強制我們在使用remove()方法之前要先進行next()方法為lastRet賦值,否則lastRet=-1,永遠都是錯誤。
      • 在next()調用之后,lastRet被賦值上了cursor,調用ArrayList.this.remove(lastRet)的方法刪掉對應的元素,完成刪除。
      • 刪除已經完成,再次將cursor=lastRet,最后lastRet=-1,為什么要設計成這樣?
a、如果在遍歷中刪除元素,會重新創建底層數組,把舊數組的值拷貝到新的數組中,后續的元素全部進一位,這樣原先next()中執行cursor=cursor+1的邏輯就亂套了,因為再次執行next()和remove()的時候我們會錯過cursor位置的元素。因此,cursor也需要在每一次執行刪除后得到修正,即cursor=lastRet,目的就是不要發生自增
b、這里把lastRet設為-1的目的是為了阻止程序員在沒有進行next()的情況下連續去remove(),只有再次執行next()之后,lastRet完成刷新,remove()方法才知道應該刪除哪個元素。
 
             這就解釋了Iterator.remove()不會出現刪除元素錯位的現象,即若上次執行了刪除,下次刪除的仍是該位置的元素。      
          

 

   4、總結

  • Iterator提供了一整套精密的流程,確保整個刪除、取值的邏輯正確,環環相扣,缺了每一步都可能出錯。

        hasNext()的目的是判定循環是否超界,next()的目的是提供向下取值,通過cursor=cursor+1相聯系,兩者組合即替代了傳統的for循環遍歷。

         next()和remove()也是按照順序去執行的,通過lastRet=cursor、lastRet=-1和lastRet=i相聯系的,兩者組合確保了remove()方法順利進行。 這三個方法就像齒輪一樣,一環扣一環,非常精妙的設計。

  •  可以從迭代器的角度來證明ArrayList、Vector這些繼承了 AbstractList的集合是非線程安全的。

         因為modCount這個成員變量存放了信息,所以不是線程安全的。試想多個線程都用迭代器去操作同一個對象,modCount發生改變,其余線程的迭代操作也會報錯。     

          

            

 

          

       


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM