1.HashMap 數據結構
數組加鏈表
數組的結構大概是這樣的
當我們put<k,v>值進去的時候 HashMap會根據key進行一個hash算法去計算一個值與數組長度(n-1)做與&運算 算出index ,這個index類似數組地址位置。
//進行hash算法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//put方法內部邏輯 省略很多
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
// i =(n - 1) & hash 算出key的下標位置
tab[i] = newNode(hash, key, value, null);
由於hash 2個不同的key 有概率會導致hash值一樣
例如put(k1,v1)和put(k2,v2) 有可能出現hash(k1) ==hash(k2)
這時候就需要用到鏈表的結構了。這個時候put(k2,v2) 就會加入到鏈表當中。
鏈表中的每個節點 Node <k,v> 都會有hash key 和value 以及指向的下個節點
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;//指向的節點
....
}
2 頭插法 和尾插法
在java8之前 新的加入的節點採用的是頭插法 就是說新加入的節點會取代原來的值,其他值順着鏈表往後移動。
原因 :作者認爲 新插入的值被查找的機率更大。這樣可以提升查找效率。
但是在java8之後採用的是尾插法 、
主要原因 :頭插法有個弊端 在多線程插入的時候由於當數組進行擴容的時候resize() 多個線程put 時候可能會出現環形鏈表如果這個時候去取值,悲劇就出現了——Infinite Loop。
Java7在多線程操作HashMap時可能引起死循環,原因是擴容轉移後前後鏈表順序倒置,在轉移過程中修改了原來鏈表中節點的引用關係。
Java8在同樣的前提下並不會引起死循環,原因是擴容轉移後前後鏈表順序不變,保持之前節點的引用關係
3.擴容
擴容主要是負載因子LoadFactor和 當前hashmap長度Capacity 有關係 就是 負載因子 * hashmap容量 < Capacity 就會進行擴容resize()
1.創建一個新的數組 這個數組的大小是原來的長度的2倍。
2.重新 hash原來的數組內容 把舊的數組重新加到新數組
重新進行hash算法 是因爲在上面有提到 key的位置不僅和hash()這個方法有關還和數組長度有關所有需要重新計算位置。
final Node<K,V>[] resize() {
//舊的數組
Node<K,V>[] oldTab = table;
//數組長度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//要進行擴容的值
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//如果大於hashmap容量最大值則不擴容了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//擴容 數組擴大一倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//
threshold = newThr;
//新建立一個數組
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;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
//重新計算下標位置
newTab[e.hash & (newCap - 1)] = e;
....省略很多代碼
}
同時看源碼可以看到定義當鏈表的 長度大於8的時候 會將鏈表轉化爲紅黑樹。
當鏈表的長度小於6的時候 會將紅黑樹轉化爲鏈表
數組默認初始容量是16(2的n次方)這樣是爲了位運算的方便 默認容量DEFAULT_INITIAL_CAPACITY = 2的n次方時候 DEFAULT_INITIAL_CAPACITY -1 的二進制都是1 這樣我們前面說的 index的結果等同於HashCode後幾位的值。
只要輸入的HashCode本身分佈均勻,Hash算法的結果就是均勻的。
/**
* 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 default initial capacity - MUST be a power of two. 默認初始化容量16 必須是2的次方
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
接着添加操作講解。添加操作的執行流程爲:
先判斷有沒有初始化
再判斷傳入的key 是否爲空,爲空保存在table[o] 位置
key 不爲空就對key 進hash,hash 的結果再& 數組的長度就得到存儲的位置
如果存儲位置爲空則創建節點,不爲空就說明存在衝突
解決衝突HashMap 會先遍歷鏈表,如果有相同的value 就更新舊值,否則構建節點添加到鏈表頭
添加還要先判斷存儲的節點數量是否達到閾值,到達閾值要進行擴容
擴容擴2倍,是新建數組所以要先轉移節點,轉移時都重新計算存儲位置,可能保持不變可能爲舊容量+位置。
擴容結束後新插入的元素也得再hash 一遍才能插入。
獲取節點的操作和添加差不多,也是
先判斷是否爲空,爲空就在table[0] 去找值
不爲空也是先hash,&數組長度計算下標位置
再遍歷找相同的key 返回值