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 instanceof TreeNode) 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: app
鏈表轉爲紅黑樹,紅黑樹較爲複雜,因此將單獨另起一篇仔細研究學習ide
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();
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方法)this
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 -> 1六、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; // 返回新數組 }