衆所皆知map的底層結構是相似鄰接表的結構,可是進入1.8以後,鏈表模式再必定狀況下又會轉換爲紅黑樹
在JDK8中,當鏈表長度達到8,而且hash桶容量超過64(MIN_TREEIFY_CAPACITY),會轉化成紅黑樹,以提高它的查詢、插入效率底層哈希桶的數據結構是數組,因此也會涉及到擴容的問題。
當MyHashMap的容量達到threshold域值時,就會觸發擴容。擴容先後,哈希桶的長度必定會是2的次方。html
那麼爲何用紅黑樹呢?以前都是用的鏈表,以前的文章有提到鏈表的隨機訪問效率是很低的,由於須要從head一個個日後面找,那麼時間複雜度就是O(n),可是若是是紅黑樹由於紅黑樹是平衡二叉樹,說白了就是能夠索引的,那麼時間複雜度只有O(logn),這樣效率就能夠獲得很大的提升
也許有人就想問了,那爲何還搞個鏈表啊,直接用紅黑樹不就完了:
1.鏈表比紅黑樹簡單,構造一個紅黑樹要比構造鏈表複雜多了,因此在鏈表很少的狀況下,總體性能上來看,當鏈表不長的時候紅黑樹的性能不必定有鏈表高
2.還有一個節點的添加和刪除的時候,須要對紅黑樹進行旋轉,着色等操做,這個就比鏈表的操做複雜多了
3.因此爲鏈表設置一個閾值用來界定何時進行樹化,何時維持鏈表,從中間取得一個均衡是很重要的node
剛剛講到紅黑樹查找效率是O(logn)那麼8的log是3,而使用鏈表,咱們以前也有提到,源碼會進行折半查找(參考以前linkedlist源碼分析)那就是8/2 = 4 平均查找長度是4,因此在8的時候是比較合適的由於3比4小
再好比鏈表長度爲6的時候,紅黑樹會退化爲鏈表同理:6=》log=2~3 和8相似,可是6/2=3也很快,並且紅黑樹很複雜,因此是用的鏈表,至於其中的數字7的做用是緩衝一下,避免再長度爲7,8徘徊的時候會頻繁修改成紅黑樹和鏈表
還有爲何是64,參考網上記錄是:再低於64的時候容量比較小,hash碰撞的概率比較大,這種時候出現長鏈表的可能性比較大,這種緣由致使的長鏈表咱們應該避免,而是採用擴容的策略避免沒必要要的樹化數組
接下來咱們觀察一下hashmap的繼承結構,瞭解一下數據結構
0.75f負載因子太高會致使鏈表過長,查找鍵值對時間複雜度就會增高,負載因子太低會致使hash桶的個數過多,空間複雜度變高函數
注意構造函數:源碼分析
hash桶沒有再構造函數中進行初始化,而是再第一次存儲鍵值的時候進行初始化,initialCapacity返回一個大於等於初始化容量大小的最小2的冪次方post
1.插入數據的時候首先會判斷hash桶是否爲空,若是爲空會進行初始化,這是避免調用構造函數以後沒有數據致使,並且再初始化的時候會調用擴容策略這個後面再講
經過剛剛的學習咱們知道hashmap有三種數據存放模式:數組,鏈表,紅黑樹
判斷是否爲空,若是爲空,直接數組存放
這裏有個細節性能
hash(key)和(n - 1) & hash 的使用
第一個對key進行hash取值學習
這裏是由於hashcode是32位的數據,用hashcode和n相與的時候,若是n比較小,那麼高位的數據基本就沒用到(2的16次冪以上的數據),那麼就會致使hash碰撞的機率加大
這裏hash(key)的操做是吧hashcode右移16位在和原來的hashcode進行異或操做,至關因而吧高位的信息合併到低位上,而後在和n作與運算,這樣高位低位的信息所有都有,綜合的話hash碰撞的機率相應減低this
------------------------------------------------------------------------------------------------------------------------------------
說明一下,這兩個操做都是取餘操做,以前有人說是取模,這裏科普一下,取模和取餘是不同的
取模(百度百科):取模運算(「Module Operation」)和取餘運算(「Complementation 」)兩個概念有重疊的部分但又不徹底一致。主要的區別在於對負整數進行除法運算時操做不一樣。取模主要是用於計算機術語中。取餘則更可能是數學概念。模運算在數論和程序設計中都有着普遍的應用,從奇偶數的判別到素數的判別,從模冪運算到最大公約數的求法,從孫子問題到凱撒密碼問題,無不充斥着模運算的身影。雖然不少數論教材上對模運算都有必定的介紹,但多數都是以純理論爲主,對於模運算在程序設計中的應用涉及很少。
7 mod 4 = 3(商 = 1 或 2,1<2,取商=1)
-7 mod 4 = 1(商 = -1 或 -2,-2<-1,取商=-2)
7 mod -4 = -1(商 = -1或-2,-2<-1,取商=-2)
-7 mod -4 = -3(商 = 1或2,1<2,取商=1)
R = a -c*b
好比-7 mod 4 => -7 = 1 -2 * 4
求模運算和求餘運算在第一步不一樣: 取餘運算在取c的值時,向0 方向舍入(fix()函數);而取模運算在計算c的值時,向負無窮方向舍入(floor()函數)。
符號相同時,二者不會衝突。好比,7/3=2.3,產生了兩個商2和37=3*2+1或7=3*3+(-2)。所以,7rem3=1,7mod3=1。符號不一樣時,二者會產生衝突。好比,7/(-3)=-2.3,產生了兩個商-2和-37=(-3)*(-2)+1或7=(-3)*(-3)+(-2)。所以,7rem(-3)=1,7mod(-3)=(-2)
------------------------------------------------------------------------------------------------------------------------------------
好的,咱們繼續討論(n-1)&hash和hash%n的問題
以前也有說到hashmap的擴容策略是大於等於初始化容量大小的最小2的冪次方,那麼也就是說n是2的倍數,轉換成2進制也就是最低位是0,再進行-1,那就是奇數
並且進行&操做
這裏注意咱們的n是2的屢次冪,那麼就是000100000000相似這樣的二進制,減一的結果就是除了最高位其他一下都是1也就是:000011111111111
這個時候和原來的數據hash作&操做,就會把超出這個length範圍的數據所有設置爲0,也就是這個範圍之內的數據不會變
Example:
8 =》 0000 0000 0000 1000
8 - 1 =》 0000 0000 0000 0111
而後不論什麼數據與8-1作&操做,那麼範圍都在 0111以內,也就是7之內包含7範圍再0~7,這樣懂了吧,好比1000000&(7-1)結果就是0~7
固然出現這種狀況有個必要的條件就是長度必須是2的n次冪,這樣再二進制數列中,永遠只有一個位置是1,其他位置是0,-1以後,這個位置一下的數據全包含再裏面&就是截取低位的數據,吧高位去掉,至關因而取餘了
由於不論什麼數字都是x = a1*2^(n-1) + a2*2^(n-2) + … + a(n-1)*2^(1) + a(n)*2^(0),高位的確定都是2的y次冪的倍數,因此去掉倍數,剩下的就是餘數,不知道我這麼說你們有沒有理解。。。
你們還能夠看看我以前的博客:https://www.cnblogs.com/cutter-point/p/11091727.html
若是不爲空那麼就要進行鏈表化或者樹化了
說白了就是再hash桶的數組上獲取這個位置上的node節點,而後循環遍歷獲取到最後一個節點,而後插入到節點末尾
//鏈表存放 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { //鏈表尾部插入,p的next判斷是否爲空 p.next = newNode(hash, key, value, null); //當鏈表的長度大於等於樹化閥值,而且hash桶的長度大於等於MIN_TREEIFY_CAPACITY,鏈表轉化爲紅黑樹 // if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // treeifyBin(tab, hash); break; } //鏈表中包含鍵值對 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; }
紅黑樹的變換規則能夠參考我以前的博客:http://www.javashuo.com/article/p-sdiixoyg-ke.html
咱們何時會進行樹化呢???
就是當咱們的鏈表長度超過或等於8個的時候
至於如何吧這個鏈表組建爲紅黑樹,這個之後單獨開章節細細探討。。。。
//數組擴容 public Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; //若是舊hash桶不爲空 if (oldCap > 0) { ////超過hash桶的最大長度,將閥值設爲最大值 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //新的hash桶的長度2被擴容沒有超過最大長度,將新容量閥值擴容爲之前的2倍 //擴大一倍以後,小於最大值,而且大於最小值 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //左移1位,也就是擴大2倍 newThr = oldThr << 1; } else if (oldThr > 0) //若是舊的容量爲空,判斷閾值是否大於0,若是是那麼就把容量設置爲當前閾值 newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //若是閾值仍是0,從新計算閾值 if (newThr == 0) { //當HashMap的數據大小>=容量*加載因子時,HashMap會將容量擴容 float ft = (float)newCap * loadFactor; //若是容量還沒超MAXIMUM_CAPACITY的loadFactor時候,那麼就返回ft,不然就是反饋int的最大值 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //hash桶的閾值 threshold = newThr; //初始化hash桶 @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { //遍歷舊數組 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //若是舊的hash桶不爲空,須要將舊的hash表裏的鍵值對從新映射到新的hash桶中 if ((e = oldTab[j]) != null) { oldTab[j] = null; //只有一個節點,經過索引位置直接映射 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //取餘 //若是是紅黑樹,須要進行樹拆分而後映射 // else if (e instanceof TreeNode) // ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order //若是是多個節點的鏈表,將原鏈表拆分爲兩個鏈表,兩個鏈表的索引位置,一個爲原索引,一個爲原索引加上舊Hash桶長度的偏移量 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; // 在遍歷原hash桶時的一個鏈表時,由於擴容後長度爲原hash表的2倍,假設把擴容後的hash表分爲兩半,分爲低位和高位, // 若是能把原鏈表的鍵值對, 一半放在低位,一半放在高位,這樣的索引效率是最高的 //這裏的方式是e.hash & oldCap, //通過rehash以後,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。對應的就是下方的resize的註釋 //爲何是移動2次冪呢??注意咱們計算位置的時候是hash&(length - 1) 那麼若是length * 2 至關於左移了一位 //也就是截取的就高了一位,若是高了一位的那個二進制正好爲1,那麼結果也至關於加了2倍 //hash & (length * 2 - 1) = length & hash + (length - 1) & hash if ((e.hash & oldCap) == 0) { //若是這個爲0,那麼就放到lotail鏈表 if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { //若是length & hash 不爲0,說明擴容以後位置不同了 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; //而這個loTail鏈表就放在原來的位置上 newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; //由於擴容了2倍,那麼新位置就能夠是原來的位置,右移一倍原始容量的大小 newTab[j + oldCap] = hiHead; } } } } } return newTab; }
總結就是擴容的時候吧數組大小擴大一倍,至關於左移1位,而且要從新計算hash散列值,找對應的位置填充
鏈表也要進行拆分,鏈表的拆分主要就體如今:
若是原來hash索引的位置就是這裏,那麼仍是鏈接再原來的節點上,若是取餘到對應的位置的節點,數組擴大一倍,咱們原來的計算方式是hash&(n - 1)
那麼若是咱們大小擴大一倍結果就是:hash&(2n - 1)=hash&n + hash&(n-1)由於n是2的n次冪,除了對應的位置爲1其他位置都爲0
那麼這裏就能夠轉換爲hash&(2n - 1)=hash&n + hash&(n-1) => n + hash&(n-1) => oldIndex + oldCap 也就是舊索引位置加上舊的容量大小
查找對於紅黑樹部分咱們略過:
至於其餘部分,也就是跟以前大同小異了,仍是hash取位置,而後取餘獲取對應的索引下標
首先檢查是否是第一個,若是是那就直接返回了
若是不是循環遍歷鏈表找到對應的key爲止
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //注意這一步中(n - 1) & hash 的值 等同於 hash(k)%table.length if ((tab = table) != null && (n = tab.length) > 0 && //這裏是計算至關因而取餘的索引位置(n - 1) & hash 等價於hash % n //並且因爲hashmap中的length再tableSizeFor的時候,就把長度設置爲2的n次冪了,那麼n-1以後的值,就是最高位全都是0,下面位數全是1 //這個也就是取hash的低位的值 (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { //暫時不考慮紅黑樹 // if (first instanceof TreeNode) // return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
4.1 樹形退化
紅黑樹,咱們就略過吧,這裏篇幅有限不作探討。。。。
這裏能夠講講hashmap的特殊地方了
1.hashmap是容許null鍵和值的,而hashtable就不容許了
參考:https://juejin.im/post/5a7719456fb9a0633e51ae14https://blog.csdn.net/xingfei_work/article/details/79637878https://juejin.im/post/5bed97616fb9a049b77fefbfhttps://www.zhihu.com/question/30526656https://juejin.im/post/5cb09c85e51d456e3428c0cf