最近作了一個面試題解答的開源項目,你們能夠看一看,若是對你們有幫助,但願你們幫忙給一個star,謝謝各位大佬了!java
《面試指北》項目地址:https://github.com/NotFound9/...git
下面是主要是本身看了《瘋狂Java講義》和一些Java容器類相關的博客,以及不少面經中涉及到的Java容器相關的面試題後,本身所有手寫的解答,也花了一些流程圖,以後會繼續更新這一部分。github
流程圖以下:面試
判斷table是否爲空或爲null,不然執行resize()方法(resize方法通常是擴容時調用,也能夠調用來初始化table)。api
根據鍵值key計算hash值。(由於hashCode是一個int類型的變量,是4字節,32位,因此這裏會將hashCode的低16位與高16位進行一個異或運算,來保留高位的特徵,以便於獲得的hash值更加均勻分佈)數組
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
根據(n - 1) & hash計算獲得插入的數組下標i,而後進行判斷緩存
那麼說明當前數組下標下,沒有hash衝突的元素,直接新建節點添加。安全
判斷table[i]的首個元素是否和key同樣,若是相同直接更新value。數據結構
判斷table[i] 是否爲treeNode,即table[i] 是不是紅黑樹,若是是紅黑樹,則直接在樹中插入鍵值對。多線程
上面的判斷條件都不知足,說明table[i]存儲的是一個鏈表,那麼遍歷鏈表,判斷是否存在已有元素的key與插入鍵值對的key相等,若是是,那麼更新value,若是沒有,那麼在鏈表末尾插入一個新節點。插入以後判斷鏈表長度是否大於8,大於8的話把鏈表轉換爲紅黑樹。
插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold(通常是數組長度*負載因子0.75),若是超過,進行擴容。
源代碼以下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // tab爲空則建立 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 計算index,並對null作處理 // (n - 1) & hash 肯定元素存放在哪一個桶中,桶爲空,新生成結點放入桶中(此時,這個結點是放在數組中) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 桶中已經存在元素 else { Node<K,V> e; K k; // 節點key存在,直接覆蓋value // 比較桶中第一個元素(數組中的結點)的hash值相等,key相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 將第一個元素賦值給e,用e來記錄 e = p; // 判斷該鏈爲紅黑樹 // hash值不相等,即key不相等;爲紅黑樹結點 else if (p instanceof TreeNode) // 放入樹中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 該鏈爲鏈表 // 爲鏈表結點 else { // 在鏈表最末插入結點 for (int binCount = 0; ; ++binCount) { // 到達鏈表的尾部 if ((e = p.next) == null) { // 在尾部插入新結點 p.next = newNode(hash, key, value, null); // 結點數量達到閾值,轉化爲紅黑樹 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); // 跳出循環 break; } // 判斷鏈表中結點的key值與插入的元素的key值是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 相等,跳出循環 break; // 用於遍歷桶中的鏈表,與前面的e = p.next組合,能夠遍歷鏈表 p = e; } } // 表示在桶中找到key值、hash值與插入元素相等的結點 if (e != null) { // 記錄e的value V oldValue = e.value; // onlyIfAbsent爲false或者舊值爲null if (!onlyIfAbsent || oldValue == null) //用新值替換舊值 e.value = value; // 訪問後回調 afterNodeAccess(e); // 返回舊值 return oldValue; } } ++modCount; // 超過最大容量 就擴容 // 實際大小大於閾值則擴容 if (++size > threshold) resize(); // 插入後回調 afterNodeInsertion(evict); return null; }
這是我本身流程圖以下所示:
判斷key==null 或者 value == null,若是是,拋出空指針異常。
根據key計算hash值(計算結果跟HashMap是一致的,寫法不一樣)。
說明當前tab尚未初始化。
那麼調用initTable()方法初始化tab。(在initTable方法中,爲了控制只有一個線程對table進行初始化,當前線程會經過CAS操做對SIZECTL變量賦值爲-1,若是賦值成功,線程才能初始化table,不然會調用Thread.yield()方法讓出時間片)。
(Node<K,V> f根據hash值計算獲得數組下標,下標存儲的元素,f多是null,鏈表頭節點,紅黑樹根節點或遷移標誌節點ForwardNode)
說明當前位置尚未哈希衝突的鍵值對。
那麼根據key和value建立一個Node,使用CAS操做設置在當前數組下標下,而且break出for循環。
說明ConcurrentHashMap正在在擴容,當前的節點f是一個標誌節點,當前下標存儲的hash衝突的元素已經遷移了。
那麼當前線程會調用helpTransfer()方法來輔助擴容,擴容完成後會將tab指向新的table,而後繼續執行for循環。
說明f節點是一個鏈表的頭結點或者是紅黑樹的根節點,那麼對f加sychronize同步鎖,而後進行如下判斷:
若是是f的hash值大於0,當前數組下標存儲的是一個鏈表,f是鏈表的頭結點。
對鏈表進行遍歷,若是有節點跟當前須要插入節點的hash值相同,那麼對節點的value進行更新,不然根據key,value建立一個Node<K,V>,添加到鏈表末尾。
若是f是TreeBin類型,那麼說明當前數組下標存儲的是一個紅黑樹,f是紅黑樹的根節點,調用putTreeVal方法,插入或更新節點。
插入完成後,判斷binCount(數組下標存儲是一個鏈表時,binCount是鏈表長度),當binCount超過8時,那麼調用treeifyBin方法將鏈表轉換爲紅黑樹。最後break出for循環。
調用addCount()對當前數組長度加1,在addCount()方法中,會判斷當前元素個數是否超過sizeCtl(擴容閾值,總長度*0.75),若是是,那麼會進行擴容,若是正處於擴容過程當中,當前線程會輔助擴容。
主要從底層數據結構,線程安全,執行效率,是否容許Null值,初始容量及擴容,hash值計算來進行分析。
transient Node<K,V>[] table; //HashMap transient volatile Node<K,V>[] table;//ConcurrentHashMap private transient Entry<?,?>[] table;//HashTable
HashMap的底層數據結構是一個數組+鏈表+紅黑樹,數組的每一個元素存儲是一個鏈表的頭結點,鏈表中存儲了一組哈希值衝突的鍵值對,經過鏈地址法來解決哈希衝突的。爲了不鏈表長度過長,影響查找元素的效率,當鏈表的長度>8時,會將鏈表轉換爲紅黑樹,鏈表的長度<6時,將紅黑樹轉換爲鏈表。之因此臨界點爲8是由於紅黑樹的查找時間複雜度爲logN,鏈表的平均時間查找複雜度爲N/2,當N爲8時,logN爲3,是小於N/2的,正好能夠經過轉換爲紅黑樹減小查找的時間複雜度。
Hashtable底層數據結構跟HashMap一致,底層數據結構是一個數組+鏈表,也是經過鏈地址法來解決衝突,只是鏈表過長時,不會轉換爲紅黑樹來減小查找時的時間複雜度。Hashtable屬於歷史遺留類,實際開發中不多使用。
ConcurrentHashMap底層數據結構跟HashMap一致,底層數據結構是一個數組+鏈表+紅黑樹。只不過使用了volatile來進行修飾它的屬性,來保證內存可見性(一個線程修改了這些屬性後,會使得其餘線程中對於該屬性的緩存失效,以便下次讀取時取最新的值)。
HashMap是非線程安全的。(例如多個線程插入多個鍵值對,若是兩個鍵值對的key哈希衝突,可能會使得兩個線程在操做同一個鏈表中的節點,致使一個鍵值對的value被覆蓋)
HashTable是線程安全的,主要經過使用synchronized關鍵字修飾大部分方法,使得每次只能一個線程對HashTable進行同步修改,性能開銷較大。
ConcurrentHashMap是線程安全的,主要是經過CAS操做+synchronized來保證線程安全的。
往ConcurrentHashMap中插入新的鍵值對時,若是對應的數組下標元素爲null,那麼經過CAS操做原子性地將節點設置到數組中。
//這是添加新的鍵值對的方法 final V putVal(K key, V value, boolean onlyIfAbsent) { ...其餘代碼 if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // 由於對應的數組下標元素爲null,因此null做爲預期值,new Node<K,V>(hash, key, value, null)做爲即將更新的值,只有當內存中的值與即將預期值一致時,纔會進行更新,保證原子性。 } ...其餘代碼 } static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); }
往ConcurrentHashMap中插入新的鍵值對時,若是對應的數組下標元素不爲null,那麼會對數組下標存儲的元素(也就是鏈表的頭節點)加synchronized鎖, 而後進行插入操做,
Node<K,V> f = tabAt(tab, i = (n - 1) & hash)); synchronized (f) {//f就是數組下標存儲的元素 if (tabAt(tab, i) == f) { if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } }
由於HashMap是非線程安全的,執行效率會高一些,其次是ConcurrentHashMap,由於HashTable在進行修改和訪問時是對整個HashTable加synchronized鎖,因此效率最低。
HashMap的key和null均可覺得null,若是key爲null,那麼計算的hash值會是0,最終計算獲得的數組下標也會是0,因此key爲null的鍵值對會存儲在數組中的首元素的鏈表中。value爲null的鍵值對也能正常插入,跟普通鍵值對插入過程一致。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
HashTable的鍵和值都不能爲null,若是將HashTable的一個鍵值對的key設置爲null,由於null值無法調用hashCode()方法獲取哈希值,因此會拋出空指針異常。一樣value爲null時,在put方法中會進行判斷,而後拋出空指針異常。
public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { throw new NullPointerException(); } Entry<?,?> tab[] = table; int hash = key.hashCode(); ...其餘代碼 }
ConcurrentHashMap的鍵和值都不能爲null,在putVal方法中會進行判斷,爲null會拋出空指針異常。
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); ...其餘代碼 }
若是不指定初始容量,HashMap和ConcurrentHashMap默認會是16,HashTable的容量默認會是11。
若是制定了初始容量,HashMap和ConcurrentHashMap的容量會是比初始容量稍微大一些的2的冪次方大小,HashTable會使用初始容量,
擴容時,HashMap和ConcurrentHashMap擴容時會是原來長度的兩倍,HashTable則是2倍加上1.
HashTable會擴容爲2n+1,HashTable之因此容量取11,及擴容時是是2n+1,是爲了保證 HashTable的長度是一個素數,由於數組的下標是用key的hashCode與數組的長度取模進行計算獲得的,而當數組的長度是素數時,能夠保證計算獲得的數組下標分佈得更加均勻,能夠看看這篇文章http://zhaox.github.io/algori...
public synchronized V put(K key, V value) { ...其餘代碼 // Makes sure the key is not already in the hashtable. Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; ...其餘代碼 }
HashMap和ConcurrentHashMap的hash值都是經過將key的hashCode()高16位與低16位進行異或運算(這樣能夠保留高位的特徵,避免一些key的hashCode高位不一樣,低位相同,形成hash衝突),獲得hash值,而後將hash&(n-1)計算獲得數組下標。(n爲數組的長度,由於當n爲2的整數次冪時,hash mod n的結果在數學上等於hash&(n-1),並且計算機進行&運算更快,因此這也是HashMap的長度老是設置爲2的整數次冪的緣由)
//HashMap計算hash值的方法 static int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } //ConcurrentHashMap計算hash值的方法 static int spread(int h) {//h是對象的hashCode return (h ^ (h >>> 16)) & HASH_BITS;// HASH_BITS = 0x7fffffff; }
在JDK1.8之後,不須要rehash,由於鍵值對的Hash值主要是根據key的hashCode()的高16位與低16位進行異或計算後獲得,根據hash%length,計算獲得數組的下標index,由於length是2的整數次冪,當擴容後length變爲原來的兩倍時,hash%(2*length)的計算結果結果差異在於第length位的值是1仍是0,若是是0,那麼在新數組中的index與舊數組的一直,若是是1,在新數組中的index會是舊數組中的數組中的index+length。
在沒有指定初始長度的狀況下,HashMap數組的默認長度爲16,在添加一個新的鍵值對時,會調用putVal()方法,在方法中,成功添加一個新的鍵值對之後,會判斷當前的元素個數是否超過閥值(數組長度*負載因子0.75),若是超過那麼調用resize方法進行擴容。具體的擴容步驟以下:
那麼直接初始化一個數組長度爲16的數組返回。
那麼直接返回舊table,不進行擴容。
將table的length擴容爲2倍,而後計算新的擴容閥值(新數組長度*0.75)。
會根據擴容後的數組長度初始化話一個新的數組,而且直接賦值給當前hashMap的成員變量table。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab;
這一步就頗有意思,也是HashMap是非線程安全的表現之一,由於此時newTab仍是一個空數組,若是有其餘線程訪問HashMap,根據key去newTab中找鍵值對,會返回null。實際上可能key是有對應的鍵值對的,只不過鍵值對都保存在舊table中,尚未遷移過來。
(與之相反,HashTable在解決擴容時其餘線程訪問的問題,是經過對大部分方法使用sychronized關鍵字修飾,也就是某個線程在執行擴容方法時,會對HashTable對象加鎖,其餘線程沒法訪問HashTable。ConcurrentHashMap在解決擴容時其餘線程訪問的問題,是經過設置ForwardingNode標識節點來解決的,擴容時,某個線程對數組中某個下標下全部Hash衝突的元素進行遷移時,那麼會將數組下標的數組元素設置爲一個標識節點ForwardingNode,以後其餘線程在訪問時,若是發現key的hash值映射的數組下標對應是一個標識節點ForwardingNode(ForwardingNode繼承於普通Node,區別字啊呀這個節點的hash值會設置爲-1,而且會多一個指向擴容過程當中新tab的指針nextTable),那麼會根據ForwardingNode中的nextTable變量,去新的tab中查找元素。(若是是添加新的鍵值對時發現是ForwardingNode,那麼輔助擴容或阻塞等待,擴容完成後去新數組中更新或插入元素)
由於HashMap的數組長度老是2的N次冪,擴容後也是變爲原來的2倍,因此有一個數學公式,當length爲2的N次冪時,
hash%length=hash&(length-1)
而由於length是2的N次冪,length-1在二進制中實際上是N-1個1。例如:
length爲16,length用2進製表示是10000,
length-1是15,用2進製表示是1111,
2*length爲32,length用2進製表示是100000,
2*length-1爲31,length用2進製表示是11111,
因此hash&(length-1)的計算結果與hash&(2*length-1)的計算結果差異在於擴容後須要多看一位,也就是看第N位的1與hash值的&結果。
假設原數組長度爲16,length-1二進制表示爲1111。key1的hash值爲9,二進制表示爲01001,key2的hash值爲25,11001,
因此hash&(length-1)的結果只要看低4位的結果,9和25的低4位都是1001,因此計算結果一致,計算結果都是9,因此在數組中處於數組下標爲9的元素鏈表中。
擴容後數組長度爲32,length-1二進制表示爲11111,key1的hash值爲9,二進制表示爲01001,key2的hash值爲25,11001,
因此hash&(2*length-1)的結果須要看低5位的結果,9和25的低4位都是1001,因此計算結果不一致,計算結果都是9和25,由於key2的hash值的第五位爲1,key1的hash值的第五位爲0,因此會多16,也就是原數組長度的大小。
因此原數組同一下標index下的鏈表存儲的hash衝突的元素,擴容後在新數組中的下標newIndex要麼爲index,要麼爲index+length(去決定於hash值的第N位爲1,仍是0,也就是hash&length的結果,原數組長度length爲2的N-1次冪)
因此會遍歷鏈表(或者紅黑樹),而後對數組下標index下每一個節點計算hash&length的結果,而後存放在兩個不一樣的臨時鏈表中,遍歷完成後,hash&length結果爲0的元素組成的臨時鏈表會存儲在新數組index位置,hash&length結果爲1的元素組成的臨時鏈表會存儲在新數組index+length位置。
HashMap默認是非線程安全的,能夠認爲每次只有一個線程來執行操做,因此hashMap就使用一個很簡單的int類型的size變量來記錄HashMap鍵值對數量就好了。
HashMap記錄鍵值對數量的實現以下:
transient int size; public int size() { return size; }
ConcurrentHashMap記錄鍵值對數量的實現以下:
//size方法最大隻能返回Integer.MAX_VALUE public int size() { long n = sumCount(); return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ?Integer.MAX_VALUE : (int)n); } //mappingCount方法能夠返回long類型的最大值, public long mappingCount() { long n = sumCount(); return (n < 0L) ? 0L : n; // ignore transient negative values } private transient volatile long baseCount; private transient volatile CounterCell[] counterCells; //sumCount會返回sumCount加上CounterCells數組中每一個元素as存儲的value final long sumCount() { CounterCell[] as = counterCells; CounterCell a; long sum = baseCount; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; } @sun.misc.Contended //這個註解能夠避免僞共享,提高性能。加與不加,性能差距達到了 5 倍。在緩存系統中,因爲一個緩存行是出於32-256個字節之間,常見的緩存行爲64個字節。而通常的變量可能達不到那麼多字節,因此會出現多個相互獨立的變量存儲在一個緩存行中的狀況,此時即使多線程訪問緩存行上相互獨立變量時,也涉及到併發競爭,會有性能開銷,加了@sun.misc.Contended這個註解,在jDK8中,會對對象先後都增長128字節的padding,使用2倍於大多數硬件緩存行的大小來避免相鄰扇區預取致使的僞共享衝突。 static final class CounterCell { volatile long value; CounterCell(long x) { value = x; } }
每次添加x個新的鍵值對後,會調用addCount()方法使用CAS操做對baseCount+x,若是操做失敗,那麼會新建一個CounterCell類型的對象,保存新增的數量x,而且將對象添加到CounterCells數組中去。
由於HashMap是非線程安全的,默認單線程環境中使用,若是get(key)爲null,能夠經過containsKey(key)
方法來判斷這個key的value爲null,仍是不存在這個key,而ConcurrentHashMap,HashTable是線程安全的,
在多線程操做時,由於get(key)和containsKey(key)兩個操做和在一塊兒不是一個原子性操做,
可能在執行中間,有其餘線程修改了數據,因此沒法區分value爲null仍是不存在key。
至於ConcurrentHashMap,HashTable的key不能爲null,主要是設計者的設計意圖。
HashMap主要是用於存儲非重複鍵值對,HashSet存儲非重複的對象。雖然HashMap是繼承於AbstractMap,實現了Map接口,HashSet繼承於AbstractSet,實現了Set接口。可是因爲它們都有去重的需求,因此HashSet主要實現都是基於HashMap的(若是須要複用一個類,咱們可使用繼承模式,也可使用組合模式。組合模式就是將一個類做爲新類的組成部分,以此來達到複用的目的。)例如,在HashSet類中,有一個HashMap類型的成員變量map,這就是組合模式的應用。
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { private transient HashMap<E,Object> map; private static final Object PRESENT = new Object();//佔位對象 public HashSet() { map = new HashMap<>(); } public boolean add(E e) { return map.put(e, PRESENT)==null;//佔位對象 } }
在HashSet的構造方法中,建立了一個HashMap,賦值給map屬性,以後在添加元素時,就是將元素做爲key添加到HashMap中,只不過value是一個佔位對象PRESENT。除了 clone()
、writeObject()
、readObject()
是 HashSet 本身不得不實現以外,其餘方法都是直接調用 HashMap 中的方法。那麼HashMap又是如何實現key不重複的呢?
在調用HashMap的putVal方法添加新的鍵值對時,會進行以下操做:
1.根據key計算hash值。
2.根據hash值映射數組下標,而後獲取數組下標的對應的元素。
3.數組下標存儲的是一個鏈表,鏈表包含了哈希衝突的元素,會對鏈表進行遍歷,判斷hash1==hash2,除此之外,還必需要key1==key2,或者key1.equals(key2)。
由於兩個不一樣的對象的hashCode可能相等,可是相同的對象的hashCode確定相等,
==是判斷兩個變量或實例是否是指向同一個內存地址,若是是同一個內存地址,對象確定相等。
int hash = hash(key);//根據key計算hash值 p = tab[i = (n - 1) & hash];//根據hash值映射數組下標,而後獲取數組下標的對應的元素。 for (int binCount = 0; ; ++binCount) {//數組下標存儲的是一個鏈表,鏈表包含了哈希衝突的元素,會對鏈表進行遍歷,判斷每一個節點的hash值與插入元素的hash值是否相等,而且是存儲key對象的地址相等,或者key相等。 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; }
首先結論以下:
第1種方法 - for-each遍歷HashMap.entrySet,使用HashMap.remove()刪除(結果:拋出異常)。
第2種方法-for-each遍歷HashMap.keySet,使用HashMap.remove()刪除(結果:拋出異常)。
第3種方法-使用HashMap.entrySet().iterator()遍歷刪除(結果:正確刪除)。
下面讓咱們來詳細探究一下緣由吧!
HashMap的遍歷刪除方法與ArrayList的大同小異,只是api的調用方式不一樣。首先初始化一個HashMap,咱們要刪除key包含"3"字符串的鍵值對。
HashMap<String,Integer> hashMap = new HashMap<String,Integer>(); hashMap.put("key1",1); hashMap.put("key2",2); hashMap.put("key3",3); hashMap.put("key4",4); hashMap.put("key5",5); hashMap.put("key6",6);
for (Map.Entry<String,Integer> entry: hashMap.entrySet()) { String key = entry.getKey(); if(key.contains("3")){ hashMap.remove(entry.getKey()); } System.out.println("當前HashMap是"+hashMap+" 當前entry是"+entry); }
輸出結果:
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前entry是key1=1 當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前entry是key2=2 當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前entry是key5=5 當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前entry是key6=6 當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4} 當前entry是key3=3 Exception in thread "main" java.util.ConcurrentModificationException at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429) at java.util.HashMap$EntryIterator.next(HashMap.java:1463) at java.util.HashMap$EntryIterator.next(HashMap.java:1461) at com.test.HashMapTest.removeWayOne(HashMapTest.java:29) at com.test.HashMapTest.main(HashMapTest.java:22)
Set<String> keySet = hashMap.keySet(); for(String key : keySet){ if(key.contains("3")){ keySet.remove(key); } System.out.println("當前HashMap是"+hashMap+" 當前key是"+key); }
輸出結果以下:
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前key是key1 當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前key是key2 當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前key是key5 當前HashMap是{key1=1, key2=2, key5=5, key6=6, key3=3, key4=4} 當前key是key6 當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4} 當前key是key3 Exception in thread "main" java.util.ConcurrentModificationException at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429) at java.util.HashMap$KeyIterator.next(HashMap.java:1453) at com.test.HashMapTest.removeWayTwo(HashMapTest.java:40) at com.test.HashMapTest.main(HashMapTest.java:23)
Iterator<Map.Entry<String, Integer>> iterator = hashMap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, Integer> entry = iterator.next(); if(entry.getKey().contains("3")){ iterator.remove(); } System.out.println("當前HashMap是"+hashMap+" 當前entry是"+entry); }
輸出結果:
當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 當前entry是key1=1 當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 當前entry是key2=2 當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 當前entry是key5=5 當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 當前entry是key6=6 當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4, deletekey=3} 當前entry是key4=4 當前HashMap是{key1=1, key2=2, key5=5, key6=6, key4=4} 當前entry是deletekey=3
第1種方法和第2種方法拋出ConcurrentModificationException異常與上面ArrayList錯誤遍歷-刪除方法的緣由一致,HashIterator也有一個expectedModCount,在遍歷時獲取下一個元素時,會調用next()方法,而後對
expectedModCount和modCount進行判斷,不一致就拋出ConcurrentModificationException異常。
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; } }
根據ConcurrentModificationException的文檔介紹,一些對象不容許併發修改,當這些修改行爲被檢測到時,就會拋出這個異常。(例如一些集合不容許一個線程一邊遍歷時,另外一個線程去修改這個集合)。
一些集合(例如Collection, Vector, ArrayList,LinkedList, HashSet, Hashtable, TreeMap, AbstractList, Serialized Form)的Iterator實現中,若是提供這種併發修改異常檢測,那麼這些Iterator能夠稱爲是"fail-fast Iterator",意思是快速失敗迭代器,就是檢測到併發修改時,直接拋出異常,而不是繼續執行,等到獲取到一些錯誤值時在拋出異常。
異常檢測主要是經過modCount和expectedModCount兩個變量來實現的,
集合被修改的次數,通常是被集合(ArrayList之類的)持有,每次調用add(),remove()方法會致使modCount+1
期待的modCount,通常是被Iterator(ArrayList.iterator()方法返回的iterator對象)持有,通常在Iterator初始化時會賦初始值,在調用Iterator的remove()方法時會對expectedModCount進行更新。(能夠看看上面的ArrayList.Itr源碼)
而後在Iterator調用next()遍歷元素時,會調用checkForComodification()方法比較modCount和expectedModCount,不一致就拋出ConcurrentModificationException。
單線程操做Iterator不當時也會拋出ConcurrentModificationException異常。(上面的例子就是)
由於ArrayList和HashMap的Iterator都是上面所說的「fail-fast Iterator」,Iterator在獲取下一個元素,刪除元素時,都會比較expectedModCount和modCount,不一致就會拋出異常。
因此當使用Iterator遍歷元素(for-each遍歷底層實現也是Iterator)時,須要刪除元素,必定須要使用 Iterator的remove()方法 來刪除,而不是直接調用ArrayList或HashMap自身的remove()方法,不然會致使Iterator中的expectedModCount沒有及時更新,以後獲取下一個元素或者刪除元素時,expectedModCount和modCount不一致,而後拋出ConcurrentModificationException異常。