JDK原始碼--HashMap(之resize)
阿新 • • 發佈:2018-11-17
1.HashMap原始碼閱讀目標
瞭解具體的資料結構(hash及衝突連結串列、紅黑樹)和重要方法的具體實現(hashCode、equals、put、resize...)
2.重要方法
hashCode 與 equals都是在AbstractMap中定義的
hashCode是各元素hash的累加 h += iter.next().hashCode();
equals 1.是否是本身; 2.是否是Map例項; 3.size是否相等; 4.比較每個value
重點在於put、resize具體實現步驟:
put:
1.tab為null或length為0 重新resize
2.位置hash(key) & (n-1)的元素為null,則直接賦值
3.既然對應位置的元素不為null,則要看它有什麼型別(單個元素(hash無衝突)或紅黑樹或連結串列)
單個元素(新的元素如果與這個元素不相等)則要轉為連結串列,連結串列則可能轉為紅黑樹(轉化規則 >= 7)
++modCount
4.++size > threshold 則resize()
remove類似(<=6則轉化為連結串列)
1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 2 boolean evict) { 3 Node<K,V>[] tab; Node<K,V> p; int n, i; 4 if ((tab = table) == null || (n = tab.length) == 0) 5 n = (tab = resize()).length; 6 if ((p = tab[i = (n - 1) & hash]) == null) 7 tab[i] = newNode(hash, key, value, null); 8 else { 9 Node<K,V> e; K k; 10 if (p.hash == hash && 11 ((k = p.key) == key || (key != null && key.equals(k)))) 12 e = p; 13 else if (p instanceofTreeNode) 14 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 15 else { 16 for (int binCount = 0; ; ++binCount) { 17 if ((e = p.next) == null) { 18 p.next = newNode(hash, key, value, null); 19 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 20 treeifyBin(tab, hash); 21 break; 22 } 23 if (e.hash == hash && 24 ((k = e.key) == key || (key != null && key.equals(k)))) 25 break; 26 p = e; 27 } 28 } 29 if (e != null) { // existing mapping for key 30 V oldValue = e.value; 31 if (!onlyIfAbsent || oldValue == null) 32 e.value = value; 33 afterNodeAccess(e); 34 return oldValue; 35 } 36 } 37 ++modCount; 38 if (++size > threshold) 39 resize(); 40 afterNodeInsertion(evict); 41 return null; 42 }
treeifyBin:
連結串列轉為紅黑樹,紅黑樹較為複雜,所以將單獨另起一篇仔細研究學習
keySet/entrySet:
1 new KeySet(); 2 forEach: 3 int mc = modCount; 4 for (int i = 0; i < tab.length; ++i) { 5 for (Node<K,V> e = tab[i]; e != null; e = e.next) 6 action.accept(e.key); 7 } 8 if (modCount != mc) 9 throw new ConcurrentModificationException();View Code
resize:(重點在此)
JDK1.7中,resize時,index取得時,全部採用重新hash的方式進行了。JDK1.8對這個進行了改善。 以前要確定index的時候用的是(e.hash & oldCap-1),是取模取餘,而這裡用到的是(e.hash & oldCap),它有兩種結果,一個是0,一個是oldCap, 比如oldCap=8,hash是3,11,19,27時,(e.hash & oldCap)的結果是0,8,0,8,這樣3,19組成新的連結串列,index為3;而11,27組成新的連結串列,新分配的index為3+8; JDK1.7中重寫hash是(e.hash & newCap-1),也就是3,11,19,27對16取餘,也是3,11,3,11,和上面的結果一樣,但是index為3的連結串列是19,3,index為3+8的連結串列是 27,11,也就是說1.7中經過resize後資料的順序變成了倒敘,而1.8沒有改變順序。 原理: 我們使用的是2次冪的擴充套件(指長度擴為原來2倍),所以,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。看下圖可以明白這句話的意思,n為table的長度,圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例,圖(b)表示擴容後key1和key2兩種key確定索引位置的示例,其中hash1是key1對應的雜湊與高位運算結果。
元素在重新計算hash之後,因為n變為2倍,那麼n-1的mask範圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:
因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,可以看看下圖為16擴充為32的resize示意圖:
這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由於新增的1bit是0還是1可以認為是隨機的,因此resize的過程,均勻的把之前的衝突的節點分散到新的bucket了。這一塊就是JDK1.8新增的優化點。有一點注意區別,JDK1.7中rehash的時候,舊連結串列遷移新連結串列的時候,如果在新表的陣列索引位置相同,則連結串列元素會倒置,但是從上圖可以看出,JDK1.8不會倒置。 --------------------- 作者:bnmb888 來源:CSDN 原文:https://blog.csdn.net/bnmb888/article/details/77164485 版權宣告:本文為博主原創文章,轉載請附上博文連結!
上面的這位博主已經說的十分清楚了,鄙人也就不獻醜了^_^, 詳情可移步原博。
詳細原始碼解析:在這裡引用的是另一博主的(老艮頭--JDK8:HashMap原始碼解析:resize方法)
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; //當前所有元素所在的陣列,稱為老的元素陣列 int oldCap = (oldTab == null) ? 0 : oldTab.length; //老的元素陣列長度 int oldThr = threshold; // 老的擴容閥值設定 int newCap, newThr = 0; // 新陣列的容量,新陣列的擴容閥值都初始化為0 if (oldCap > 0) { // 如果老陣列長度大於0,說明已經存在元素 // PS1 if (oldCap >= MAXIMUM_CAPACITY) { // 如果陣列元素個數大於等於限定的最大容量(2的30次方) // 擴容閥值設定為int最大值(2的31次方 -1 ),因為oldCap再乘2就溢位了。 threshold = Integer.MAX_VALUE; return oldTab; // 返回老的元素陣列 } /* * 如果陣列元素個數在正常範圍內,那麼新的陣列容量為老的陣列容量的2倍(左移1位相當於乘以2) * 如果擴容之後的新容量小於最大容量 並且 老的陣列容量大於等於預設初始化容量(16),那麼新陣列的擴容閥值設定為老閥值的2倍。(老的陣列容量大於16意味著:要麼建構函式指定了一個大於16的初始化容量值,要麼已經經歷過了至少一次擴容) */ else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // PS2 // 執行到這個else if 說明老陣列沒有任何元素 // 如果老陣列的擴容閥值大於0,那麼設定新陣列的容量為該閥值 // 這一步也就意味著構造該map的時候,指定了初始化容量。 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults // 能執行到這裡的話,說明是呼叫無參建構函式建立的該map,並且第一次新增元素 newCap = DEFAULT_INITIAL_CAPACITY; // 設定新陣列容量 為 16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 設定新陣列擴容閥值為 16*0.75 = 12。0.75為負載因子(當元素個數達到容量了4分之3,那麼擴容) } // 如果擴容閥值為0 (PS2的情況) if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); // 參見:PS2 } threshold = newThr; // 設定map的擴容閥值為 新的閥值 @SuppressWarnings({"rawtypes","unchecked"}) // 建立新的陣列(對於第一次新增元素,那麼這個陣列就是第一個陣列;對於存在oldTab的時候,那麼這個陣列就是要需要擴容到的新陣列) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 將該map的table屬性指向到該新陣列 if (oldTab != null) { // 如果老陣列不為空,說明是擴容操作,那麼涉及到元素的轉移操作 for (int j = 0; j < oldCap; ++j) { // 遍歷老陣列 Node<K,V> e; if ((e = oldTab[j]) != null) { // 如果當前位置元素不為空,那麼需要轉移該元素到新陣列 oldTab[j] = null; // 釋放掉老陣列對於要轉移走的元素的引用(主要為了使得陣列可被回收) if (e.next == null) // 如果元素沒有有下一個節點,說明該元素不存在hash衝突 // PS3 // 把元素儲存到新的陣列中,儲存到陣列的哪個位置需要根據hash值和陣列長度來進行取模 // 【hash值 % 陣列長度】 = 【 hash值 & (陣列長度-1)】 // 這種與運算求模的方式要求 陣列長度必須是2的N次方,但是可以通過建構函式隨意指定初始化容量呀,如果指定了17,15這種,豈不是出問題了就?沒關係,最終會通過tableSizeFor方法將使用者指定的轉化為大於其並且最相近的2的N次方。 15 -> 16、17-> 32 newTab[e.hash & (newCap - 1)] = e; // 如果該元素有下一個節點,那麼說明該位置上存在一個連結串列了(hash相同的多個元素以連結串列的方式儲存到了老陣列的這個位置上了) // 例如:陣列長度為16,那麼hash值為1(1%16=1)的和hash值為17(17%16=1)的兩個元素都是會儲存在陣列的第2個位置上(對應陣列下標為1),當陣列擴容為32(1%32=1)時,hash值為1的還應該儲存在新陣列的第二個位置上,但是hash值為17(17%32=17)的就應該儲存在新陣列的第18個位置上了。 // 所以,陣列擴容後,所有元素都需要重新計算在新陣列中的位置。 else if (e instanceof TreeNode) // 如果該節點為TreeNode型別 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 此處單獨展開討論 else { // preserve order Node<K,V> loHead = null, loTail = null; // 按命名來翻譯的話,應該叫低位首尾節點 Node<K,V> hiHead = null, hiTail = null; // 按命名來翻譯的話,應該叫高位首尾節點 // 以上的低位指的是新陣列的 0 到 oldCap-1 、高位指定的是oldCap 到 newCap - 1 Node<K,V> next; // 遍歷連結串列 do { next = e.next; // 這一步判斷好狠,拿元素的hash值 和 老陣列的長度 做與運算 // PS3裡曾說到,陣列的長度一定是2的N次方(例如16),如果hash值和該長度做與運算,結果為0,就說明該hash值一定小於陣列長度(例如hash值為1),那麼該hash值再和新陣列的長度取摸的話,還是hash值本身,所該元素的在新陣列的位置和在老陣列的位置是相同的,所以該元素可以放置在低位連結串列中。 if ((e.hash & oldCap) == 0) { // PS4 if (loTail == null) // 如果沒有尾,說明連結串列為空 loHead = e; // 連結串列為空時,頭節點指向該元素 else loTail.next = e; // 如果有尾,那麼連結串列不為空,把該元素掛到連結串列的最後。 loTail = e; // 把尾節點設定為當前元素 } // 如果與運算結果不為0,說明hash值大於老陣列長度(例如hash值為17) // 此時該元素應該放置到新陣列的高位位置上 // 例:老陣列長度16,那麼新陣列長度為32,hash為17的應該放置在陣列的第17個位置上,也就是下標為16,那麼下標為16已經屬於高位了,低位是[0-15],高位是[16-31] else { // 以下邏輯同PS4 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { // 低位的元素組成的連結串列還是放置在原來的位置 loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { // 高位的元素組成的連結串列放置的位置只是在原有位置上偏移了老陣列的長度個位置。 hiTail.next = null; newTab[j + oldCap] = hiHead; // 例:hash為 17 在老陣列放置在0下標,在新陣列放置在16下標; hash為 18 在老陣列放置在1下標,在新陣列放置在17下標; } } } } } return newTab; // 返回新陣列 }