最近去某公司面試,被問到了一個簡單的問題,ArrayList中要刪除一些元素應該怎么操作?答曰:"使用Iterator迭代器遍歷,判斷之后再用迭代器提供的remove()方法將判斷為true的元素刪掉",問:“為什么要選擇這個方法?”答曰:“迭代器采用的是fail-fast機制,foreach循環內部調用了迭代器,刪除元素破壞了集合的結構,所以會報錯”,追問:“為什么不能用for循環呢?沒使用迭代器也應該可行啊?為什么迭代器又是可以的呢?” 答曰:“因為for循環刪掉元素后會錯位,至於迭代器是怎么解決這個問題的, 還真不知道...”
作為一名菜鳥,招架不了這么連珠炮般的提問,於是回去讀了一遍關於Iterator的源碼,簡要的解析一番。本次學習過程大概是圍繞這幾個問題展開的:
1、Iterator和Iterable的關系
Iterator是java中的一個接口,定義了一系列方法。諸如hasNext()判定、next()取下一元素的值和remove()刪除當前元素值。
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.
-
為什么會報錯?
在這里就不貼代碼了,直接上結果:
-
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()方法
-
-
- next()方法
-
-
在每次該方法執行的邏輯尾部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,為什么要設計成這樣?
-
-
4、總結
- Iterator提供了一整套精密的流程,確保整個刪除、取值的邏輯正確,環環相扣,缺了每一步都可能出錯。
hasNext()的目的是判定循環是否超界,next()的目的是提供向下取值,通過cursor=cursor+1相聯系,兩者組合即替代了傳統的for循環遍歷。
next()和remove()也是按照順序去執行的,通過lastRet=cursor、lastRet=-1和lastRet=i相聯系的,兩者組合確保了remove()方法順利進行。 這三個方法就像齒輪一樣,一環扣一環,非常精妙的設計。
- 可以從迭代器的角度來證明ArrayList、Vector這些繼承了 AbstractList的集合是非線程安全的。
因為modCount這個成員變量存放了信息,所以不是線程安全的。試想多個線程都用迭代器去操作同一個對象,modCount發生改變,其余線程的迭代操作也會報錯。