一.前言
JDK1.8 Hashmap采用的是數組+鏈表+紅黑樹的數據結構
二.基本參數介紹
/** * The default initial capacity - MUST be a power of two.
* 桶的容量,默認16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30.
* 桶的最大容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30
/**
* The load factor used when none specified in constructor.
* 負載因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage.
* 樹化閥值
*/ static final int TREEIFY_THRESHOLD = 8; /** * The bin count threshold for untreeifying a (split) bin during a * resize operation. Should be less than TREEIFY_THRESHOLD, and at * most 6 to mesh with shrinkage detection under removal.
* 樹退化閥值 */ static final int UNTREEIFY_THRESHOLD = 6; /** * The smallest table capacity for which bins may be treeified. * (Otherwise the table is resized if too many nodes in a bin.) * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts * between resizing and treeification thresholds.
* 最小樹化容量閥值即容量必要大於64才開始樹化
* 避免樹化和擴容沖突
*/ static final int MIN_TREEIFY_CAPACITY = 64;
三.擴容
先看下JDK1.7Hashmap擴容源碼
明顯我們看出在JDK1.7中,先擴容,再存儲。
擴容條件:當前數量大於 容量* 負載因子 並且數組下標的值不為空,即假如新插入的數據位置在一個數組位置而不是鏈表上,則插入成功而不擴容(有人只看前面條件,后面條件被忽視)
擴容后的位置怎么計算尼?
我們看例1,假設hash為1011 1101 即349,數組容量為16,但是我們數組是從0開始計算,則數組下標實際長度為15即0000 1111,通過&運算,得到數組下標值為13。擴容后,數組容量為32,通過&運算得到數組下標值為29。
我們再看例2,假設hash為1010 1101即317,&運算得到數組下標為13。擴容后,得到數組下標值為13。
我們可以看到表格中紅色標注部分,擴容后,原值的hash受原數組容量影響。新值的下標是原下標或原下標+數組容量,如果數組存在鏈表,因為他們hash值相同,所以鏈表上的值 也跟着相應移動且位置發生倒轉(即原來鏈表順序是1,2,3 在新數組編程3,2,1)。
再看JDK1.8Hashmap擴容源碼
仔細看第一個圖,我們發現++size,即JDK1.8是先存儲,后擴容。擴容條件只有大於容量*負載因子
JDK1.8的數據結構是數組+鏈表+紅黑樹,Node<K,V>中存儲着鏈表節點next 也是Node<K,V>結構。
我們可以看出圖片標記1處 如果舊值不存在鏈表,則根據hash值和新容量&計算數組下標並賦值。但是存在鏈表
如果舊值的hash和舊的容量計算&為0,則擴容后的位置等於原來坐標。
如果舊值的hash和舊的容量計算&為1,則擴容后的位置等於原來坐標+舊的容量
四.擴展知識
- JDK1.7和JDK1.8Hashmap區別?
JDK1.7用的是頭插法,而JDK1.8及之后使用的都是尾插法。因為JDK1.7是用單鏈表進行的縱向延伸,當采用頭插法時會容易出現逆序且環形鏈表死循環問題。但是在JDK1.8之后是因為加入了紅黑樹使用尾插法,能夠避免出現逆序且鏈表死循環的問題。
擴容后數據存儲位置的計算方式也不一樣。見第三點
- 為什么負載因子不是0.5或1?
如果是0.5,臨界值是8 則很容易就觸發擴容,而且還有一半容量還沒用
如果是1,當空間被占滿時候才擴容,增加插入數據的時間
0.75即3/4,capacity值是2的冪,相乘得到結果是整數
- 為什么在JDK1.8中進行對HashMap優化的時候,把鏈表轉化為紅黑樹的閾值是8,而不是7或者5呢?
根據注釋中寫到,理想情況下,在隨機哈希碼和默認大小調整閾值為 0.75 的情況下,存儲桶中元素個數出現的頻率遵循泊松分布,平均參數為 0.5,有關 k 值下,隨機事件出現頻率的計算公式為 (exp(-0.5) * pow(0.5, k) /factorial(k)))大體得到一個數值是8,那么退化樹閥值為什么是6?如果退化樹閥值也是8,則會陷入樹化和退化的死循環中。如果退化閥值是7,假如對hash進行頻繁的增刪操作,同樣會進入死循環中。如果退化樹閥值小於5,我們知道紅黑樹在低元素查詢效率並不比鏈表高,而且紅黑樹會存儲很多索引,占有內存。所以退化閥值設為6比較合理。
- JDK1.7是先擴容再插入,而JDK1.8是先插入再擴容。為什么?
這個問題網上查找很多資料沒有明確答案。可能原因是JDK1.7采用頭插法,擴容后,計算hash,只需要插入鏈表頭部就行。而JDK1.8采用尾插法,如果先擴容,擴容后需要遍歷一遍,再找到尾部進行插入。