背景:一直以來對迭代器的問題理解不是很透徹,特別是迭代器和異常ConcurrentModificationException之間的聯繫。經過debug,詳細瞭解其底層的具體實現過程。java
Iterator必須依附於Collection對象,如有一個Iterator對象,則必然有一個與之關聯的Collection對象。面試
Iterator提供了兩個方法來迭代訪問Collection集合裏的元素,並可經過remove()來刪除集合中上一次next()方法返回的集合元素。編程
當使用Iterator迭代訪問Collection集合元素時,Collection集合裏的元素不能被改變,只有經過Iterator的remove()方法刪除上一次next()方法返回的集合元素才能夠;不然會引起java.util.ConcurrentModificationException異常。數組
之因此會出現這樣的異常,是由於Iterator迭代器採用的是快速失敗(fast-fail)機制,一旦在迭代過程當中檢測到該集合已經被修改(一般是程序中其它線程修改),程序當即引起ConcurrentModificationException,安全
而不是顯示修改後結果,這樣能夠避免共享資源而引起的潛在問題。數據結構
ConcurrentModificationException發生在Iterator#next()方法實現中,每次調用都會檢查容器的結構是否發生變化,目的是爲了不共享資源而引起的潛在問題。多線程
觀察HashMap和ArrayList底層Iterator#next(), 能夠看到fast-fail只會增長或者刪除(非Iterator#remove())拋出異常;改變容器中元素的內容不存在這個問題(主要是modCount沒發生變化)。app
在單線程中使用迭代器,對非線程安全的容器,可是隻能用Iterator#remove;不然會拋出異常。ide
在多線程中使用迭代器,可使用線程安全的容器來避免異常。工具
使用普通的for循環遍歷,效率雖然比較低下,可是不存在ConcurrentModificationException異常問題。用的也比較少。
ps:java在設計工具類時候,分別設計出線程安全和非安全的工具類,也是致力於解決這些多線程操做問題。因此無須糾結,直接使用就行。
/** * The number of times this HashMap has been structurally modified * Structural modifications are those that change the number of mappings in * the HashMap or otherwise modify its internal structure (e.g., * rehash). This field is used to make iterators on Collection-views of * the HashMap fail-fast. (See ConcurrentModificationException). */ transient int modCount;
在觀察底層實現時,能夠看到容器對象的modCount值在改變容器結構時才發生改變。
這裏說的容器結構的改變是指 增長 或者刪除元素,致使集合的大小發生改變。
觀察源碼發現,不論Collection或Map,對於Iterator來講:
異常是在next方法中拋出的,咱們在使用迭代器的時候,通常會先進行hasNext方法的判斷,再調用next方法來使用元素。
如下是對於ArrayList、 HashMap、ConcurrentHashMap三個容器的迭代器測試用例:
/** * Project Name:Spring0725 * File Name:Test5.java * Package Name:work1201.basic * Date:2017年12月1日下午4:16:25 * Copyright (c) 2017, 深圳金融電子結算中心 All Rights Reserved. * */ import java.util.ArrayList; import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.Iterator; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.junit.Test; /** * ClassName:TestIterator <br/> * Function: 測試Collection Map 改變集合結構對迭代器遍歷有無影響 * 在使用Iterator時候,對於Collection集合,改變集合的結構會觸發ConcurrentModificationException異常;改變集合中元素的內容不會觸發異常 * 對於Map集合,線程安全map——ConcurrentHashMap改變集合Map的結構,無異常發生 * 對於非線程安全的HashMap,使用Iterator遍歷的時候,改變集合的結構,會觸發ConcurrentModificationException * * 非線程安全類的Collection或者Map,在使用Iterator過程當中,有時候改變容器結構,並不會發生異常,這主要和底層的實現有關。 * 如:List刪除倒數第二個元素無異常;Map刪除set集合中的最後一個元素,無異常 * Date: 2017年12月1日 下午4:16:25 <br/> * @author prd-lxw * @version 1.0 * @since JDK 1.7 * @see */ public class TestIterator { /** * 方法1 * 迭代器更改Collection集合結構,非倒數第二個元素,會發生異常 */ @Test(expected = ConcurrentModificationException.class) public void testDeletCollection() { ArrayList<String> list = new ArrayList<String>(); for (int i = 0; i < 20; i++) { list.add(i + ""); } Iterator<String> it = list.iterator(); String ss = 19 + ""; while (it.hasNext()) { if (it.next().equals(ss)) { System.out.println("找到元素:" + ss); list.remove(ss); //集合大小發生改變 ConcurrentModificationException // list.add(102+""); //集合大小發生改變 ConcurrentModificationException // list.set(19, 211+""); //不會觸發異常,由於沒有改變Collection集合的大小 // it.remove();//正常 } } } /** * 方法2 * 刪除Collection集合的倒數第二個元素,不會發生異常 */ @Test public void testDeletBack2Collection() { ArrayList<String> list = new ArrayList<String>(); for (int i = 0; i < 20; i++) { list.add(i + ""); } Iterator<String> it = list.iterator(); String ss = 18 + "";//倒數第2個元素 while (it.hasNext()) { if (it.next().equals(ss)) { System.out.println("找到元素:" + ss); list.remove(ss); //集合大小發生改變 ConcurrentModificationException // list.add(102+""); //集合大小發生改變 ConcurrentModificationException // it.remove();//正常 } } } /** * 方法3 * 普通for方法遍歷Collection,改變集合結構無異常 */ @Test public void testDeletCollectionFor() { ArrayList<String> list = new ArrayList<String>(); for (int i = 0; i < 20; i++) { list.add(i + ""); } String ss = 15 + ""; for (int i = 0; i < list.size(); i++) { if (list.get(i).equals(ss)) { list.remove(i); } } } /** * 方法4 * 使用加強型的foreach遍歷Collection,改變集合結構,會發生異常 * 起底層實現和使用Iterator一致 */ @Test(expected = ConcurrentModificationException.class) public void testDeletCollectionForEach() { ArrayList<String> list = new ArrayList<String>(); for (int i = 0; i < 20; i++) { list.add(i + ""); } String ss = 18 + ""; for (String str : list) { if (str.equals(ss)) { list.remove(str); } } } /** * 方法5 * 使用迭代器的過程當中,改變map的結構,會觸發ConcurrentModificationException異常 * 可是若是刪除的key在entrySet的結尾,好比key=10+"" 就不會發生這個異常 */ @Test(expected = ConcurrentModificationException.class) public void testIteratorMapEntry() { HashMap<String, String> map = new HashMap<String, String>(); for (int i = 0; i < 20; i++) { map.put(i + "", i + ""); } Set<Entry<String, String>> entrySet = map.entrySet(); //打印entrySet集合能夠發現,key=10是集合的最後一個元素 Iterator<Entry<String, String>> it = entrySet.iterator(); String key = 10 + ""; while (it.hasNext()) { if (it.next().getKey().equals(key)) { System.out.println("testIteratorMapEntry找到元素:" + key); //改變Map // map.remove(key); //ConcurrentModificationException map.put(21 + "", 21 + ""); //ConcurrentModificationException // map.replace(key, 30 + "");//正常 // it.remove(); //正常 System.out.println(map.size() + ":" + map.get(key)); } } } /** * 方法6 * 使用迭代器的過程當中,改變map的結構,會觸發ConcurrentModificationException異常 * 可是若是刪除的key在keySet的結尾,好比key=10+"" 就不會發生這個異常 * * 對於非線程安全的map 使用Iterator遍歷 keySet valueSet entrySet實驗結果都一致 */ @Test(expected = ConcurrentModificationException.class) public void testIteratorMapKey() { HashMap<String, String> map = new HashMap<String, String>(); for (int i = 0; i < 20; i++) { map.put(i + "", i + ""); } Set<String> mapKeySet = map.keySet();//打印keySet集合能夠發現,key=10是集合的最後一個元素 Iterator<String> it = mapKeySet.iterator(); String key = 11 + ""; while (it.hasNext()) { if (it.next().equals(key)) { System.out.println("testIteratorMapKey找到元素:" + key); //改變Map map.remove(key); //ConcurrentModificationException // map.put(21 + "", 21 + ""); //ConcurrentModificationException // map.replace(key, 30 + "");//正常 // it.remove(); //正常 System.out.println(map.size() + ":" + map.get(key)); } } } // ################# 線程安全類ConcurrentHashMap不存在ConcurrentModificationException問題 /** * 方法7 * 線程安全類Map entrySet操做,無異常發生 */ @Test public void testConIteratorMapEntry() { ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>(); for (int i = 0; i < 20; i++) { map.put(i + "", i + ""); } Iterator<Entry<String, String>> it = map.entrySet().iterator(); String key = 12 + ""; while (it.hasNext()) { if (it.next().getKey().equals(key)) { System.out.println("testConIteratorMapEntry找到元素:" + key); //改變Map map.remove(key); //正常 // map.put(21 + "", 21 + ""); //正常 // map.replace(key, 30 + "");//正常 // it.remove(); //正常 System.out.println(map.size() + ":" + map.get(key)); } } } /** * 方法8 * 無異常 */ @Test public void testConIteratorMapKey() { ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>(); for (int i = 0; i < 20; i++) { map.put(i + "", i + ""); } Iterator<String> it = map.keySet().iterator(); String key = 10 + ""; while (it.hasNext()) { if (it.next().equals(key)) { System.out.println("testConIteratorMapKey找到元素:" + key); //改變Map // map.remove(key); //正常 map.put(21 + "", 21 + ""); //正常 map.replace(key, 30 + "");//正常 // it.remove(); //正常 System.out.println(map.size() + ":" + map.get(key)); } } } }
觀察上述方法1 和方法2 ,能夠發現一個問題——
在使用迭代器遍歷ArrayList時候,若是刪除的是鏈表中倒數第二個元素,不會發生ConcurrentModificationException異常,不然就會發生異常。
方法1 和2實現同樣,只不過刪除的元素下標不同。
分析ArrayList中Iterator中next() 和hasNext() 的具體實現
從底層實現來理解上述差別產生的緣由:
由於異常都是發生在Iterator#next()方法中,因此能夠打開iterator()方法的實現。
Iterator<String> it = list.iterator();
將ArrayList.class中關於迭代器的代碼摘錄以下
/** * Returns an iterator over the elements in this list in proper sequence. * 經過這個方法獲取到list的迭代器 內部類Itr()實現了Iterator接口 * <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>. * * @return an iterator over the elements in this list in proper sequence */ public Iterator<E> iterator() { return new Itr(); } /** * An optimized version of AbstractList.Itr */ private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; //迭代器初始化時候設置的,一旦容器結構發生變化,會改變modCount的值,進而引起後面的異常 public boolean hasNext() { return cursor != size; } @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; return (E) elementData[lastRet = i]; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } @Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer<? super E> consumer) { Objects.requireNonNull(consumer); final int size = ArrayList.this.size; int i = cursor; if (i >= size) { return; } final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); } // update once at end of iteration to reduce heap write traffic cursor = i; lastRet = i - 1; checkForComodification(); } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
從源碼能夠看到,ArrayList#iterator()調用的是Itr類,Itr實現了Iterator接口,該類中實現了接口中的next()和hasNext()方法,因此
ArrayList#iterator()#hasNext() 就是調用Itr#hasNext()
ArrayList#iterator()#next() 就是調用Itr#next()
執行hasNext()會判斷下一個元素下標與集合大小是否相等(元素存在與否),相等返回false;不等 返回true;
ps:對於一個list的,lastIndex = size-1 在執行next()方法前會進行結構是否變化的檢查,modCount != expectedModCount,返回true就拋異常,返回false就不拋出異常。 ps:一旦容器結構發生變化,modCount的值會發生變化,每次累加1;expectedModCount在生成迭代器時候進行初始化,表明初始化時候容器的modCount。.modCount是屬於ArrayList對象的,expectedModCount是屬於迭代器對象的。 每次next()方法執行完以後,cursor都表明當前元素的下一個元素的下標。
而對於方法1和方法2:
這裏討論的是非線程安全類的Map,對Map的keySet、 valueSet、 entrySet三個集合可使用Iterator進行遍歷。
這裏舉例解釋方法6中存在的問題:
map中放入0-19個元素,刪除key=10+「」會拋出異常;其它的則不會。
分析HashMap中Iterator中next() 和hasNext() 的具體實現
打開方法6中的 HashMap#keySet()實現
Set<String> mapKeSet = map.keySet();//打印keySet集合能夠發現,key=10是集合的最後一個元素 Iterator<String> it = mapKeSet.iterator();
在HashMap.class中能夠看到以下的實現
public Set<K> keySet() { Set<K> ks = keySet; if (ks == null) { ks = new KeySet(); keySet = ks; } return ks; } final class KeySet extends AbstractSet<K> { public final int size() { return size; } public final void clear() { HashMap.this.clear(); } public final Iterator<K> iterator() { return new KeyIterator(); } //Iterator接口的具體實現類 public final boolean contains(Object o) { return containsKey(o); } public final boolean remove(Object key) { return removeNode(hash(key), key, null, false, true) != null; } public final Spliterator<K> spliterator() { return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0); } public final void forEach(Consumer<? super K> action) { Node<K,V>[] tab; if (action == null) throw new NullPointerException(); if (size > 0 && (tab = table) != null) { int mc = modCount; for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) action.accept(e.key); } if (modCount != mc) throw new ConcurrentModificationException(); } } }
分析: HashMap#keySet()覆寫了父類AbstractMap#keySet()方法,跟蹤變量keySet的初始化過程,能夠發現其初始值默認爲null。
HashMap#keySet()方法中 接口Set的實現類爲KeySet,進一步跟蹤能夠發現,mapKeSet.iterator()返回的對象是實例化KeyIterator生成的對象,跟蹤改類的具體實現——
下面是Map與Iterator的核心代碼
// iterators abstract class HashIterator { Node<K,V> next; // next entry to return Node<K,V> current; // current entry int expectedModCount; // for fast-fail int index; // current slot HashIterator() { expectedModCount = modCount; Node<K,V>[] t = table; current = next = null; index = 0; if (t != null && size > 0) { // advance to first entry do {} while (index < t.length && (next = t[index++]) == null); } } public final boolean hasNext() { return next != null; } final Node<K,V> nextNode() { Node<K,V>[] t; Node<K,V> e = next; if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null) throw new NoSuchElementException(); if ((next = (current = e).next) == null && (t = table) != null) { do {} while (index < t.length && (next = t[index++]) == null); } return e; } public final void remove() { Node<K,V> p = current; if (p == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); current = null; K key = p.key; removeNode(hash(key), key, null, false, false); expectedModCount = modCount; } } final class KeyIterator extends HashIterator implements Iterator<K> { public final K next() { return nextNode().key; } } final class ValueIterator extends HashIterator implements Iterator<V> { public final V next() { return nextNode().value; } } final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> { public final Map.Entry<K,V> next() { return nextNode(); } }
咱們從源碼能夠看到 KeyIterator實現了Iterator接口,而且繼承了抽象類HashIterator,在該類中實現了接口Iterator#next()方法;而KeyIterator#next()又是調用其父類HashIterator#nextNode()方法。
因此其底層實現:
HashMap#keySet()#iterator()#hasNext() 調用的是HashIterator#hasNext()
HashMap#keySet()#iterator()#next() 調用的就是HashIterator#nextNode().key
對於Map:進行hasNext 是判斷next是否爲null,true-後面有元素。false-後面沒元素。執行next的時候,會進行map結構的檢查,modCount != expectedModCount,返回true就拋異常,返回false就不拋出異常。當咱們刪除keySet最後一個元素時候,hasNext返回false,不會進入Next,天然不發生異常當刪除非最後一個元素的時候,執行next的時候觸發結構檢查,發生異常。是不是最後一個能夠經過觀察set的輸出結果。
在方法6中,經過debug能夠觀察到mapKeySet中的key值排列以下
[11, 12, 13, 14, 15, 16, 17, 18, 19, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
當刪除key=10+「」元素的時候,迭代器循環結束,不會在進入hasNext方法,因此就沒有異常產生了。
多線程環境下的結果和單測中的結果一致,這裏只是爲了模擬觀察
/** * Project Name:Spring0725 * File Name:IteratorListDelete.java * Package Name:work12.day05 * Date:2017年12月5日下午9:58:53 * Copyright (c) 2017, 深圳金融電子結算中心 All Rights Reserved. * */ package work12.day05; import java.util.ArrayList; import java.util.Iterator; /** * ClassName:IteratorListDelete <br/> * Function: 測試多線程下 改變Collection集合 對迭代器遍歷形成的影響 Date: 2017年12月5日 下午9:58:53 <br/> * * @author prd-lxw * @version 1.0 * @since JDK 1.7 * @see */ public class IteratorListDelete { private final ArrayList<String> list; public IteratorListDelete(ArrayList<String> list) { super(); this.list = list; } public ArrayList<String> getList() { return list; } public static void main(String[] args) { ArrayList<String> aList = new ArrayList<String>(); IteratorListDelete tt = new IteratorListDelete(aList); for (int i = 0; i < 100; i++) { tt.getList().add(i + ""); } new Thread(new TraverseList(tt), "子線程").start(); // System.out.println(tt.getList().get(28)); try { Thread.sleep(10); // tt.getList().remove(28+""); // 集合大小發生改變 ConcurrentModificationException tt.getList().add(101 + ""); // 集合大小發生改變 ConcurrentModificationException // tt.getList().set(28, 201+""); //改變集合內容,不會觸發異常,由於沒有改變Collection集合的大小 } catch (InterruptedException e) { e.printStackTrace(); } } } /** * ClassName: TraverseList <br/> * Function: 線程一直循環遍歷collection集合 date: 2017年12月5日 下午10:35:06 <br/> * * @author prd-lxw * @version 1.0 * @since JDK 1.7 */ class TraverseList implements Runnable { private final IteratorListDelete tt; public TraverseList(IteratorListDelete tt) { super(); this.tt = tt; } public void run() { try { Thread.sleep(5); } catch (Exception e) { // TODO: handle exception } while (true) { Iterator<String> it = tt.getList().iterator(); while (it.hasNext()) { System.out.println(Thread.currentThread().getName() + "循環遍歷:" + it.next()); } } } }
ps:阿里面試時候問到這個問題,當時是一臉的懵逼。
迭代模式是訪問集合類的通用方法,只要集合類實現了Iterator接口,就能夠用迭代的方式來訪問集合類內部的數據,Iterator訪問方式把對不一樣集合類的訪問邏輯抽象出來,使得不用暴露集合內部的結構而達到循環遍歷集合的效果。
例如,若是沒有使用Iterator,遍歷一個數組的方法是使用索引:
這種方法的缺點就是事先必須知道集合的數據結構,並且當我換了一種集合的話代碼不可重用,要修改,好比我用set,就不能經過索引來遍歷了。訪問代碼和集合是緊耦合,沒法將訪問邏輯從集合類和客戶端代碼中剝離出來,每一種集合類對應一種訪問方式,代碼不可重用。
爲解決以上問題,Iterator模式老是用同一種邏輯來遍歷集合。
每一種集合類返回的Iterator具體類型可能不一樣,Array可能返回ArrayIterator,Set可能返回SetIterator,Tree 可能返回TreeIterator,可是它們都實現了Iterator接口,所以,客戶端不關心究竟是哪一種Iterator,它只須要得到這個 Iterator接口便可,這就是面向對象的威力。
這就是針對抽象編程的原則:對具體類的依賴性最小。