1. HashMap繼承結構
2. HashMap底層資料結構
在1.7及其之前,HashMap
底層是使用 陣列 + 連結串列實現的,在1.8及其之後,使用了 陣列 + 連結串列/紅黑樹 實現。
來看下1.7的儲存結構圖:
其中連結串列使用內部類Node來實現的:
陣列+連結串列(雜湊表) 其實就是用於解決雜湊衝突使用的一個拉鍊法
方法。在資料結構中,我們處理hash衝突常使用的方法有:開發定址法、再雜湊法、鏈地址法、建立公共溢位區。而HashMap中處理hash衝突的方法就是鏈地址法。
但是這樣子的話,如果使用了很久,HashMap儲存的元素越來越多,那麼連結串列就會變的很長,那麼效能就會下降很多(因為連結串列不適合查詢元素,每次查詢元素都要從頭開始遍歷)。
於是在1.8的時候進行了改進,使用到了紅黑樹(紅黑樹是一個自平衡的二叉查詢樹,查詢效率是非常高,時間複雜度僅為O(logN))。
在HashMap中,連結串列轉化成紅黑樹的條件是當連結串列長度大於8且陣列(桶)的個數要大雨等於64個時,才可以將連結串列轉化成紅黑樹,它們在原始碼中的定義如下:
static final int MIN_TREEIFY_CAPACITY = 64; // 轉化成紅黑樹的最小的桶容量
static final int TREEIFY_THRESHOLD = 8; // 桶上的元素的數量
treeifyBin中的片段:
// 意思是隻要桶的個數小於64個,那麼即使桶中的元素個數超過了8個,那麼就進行resize擴容,而不是轉化成紅黑樹
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
putVal中的片段:
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// -1 for 1st 可以理解為元素下表從-1開始的,所以可以看作binCount >= 9
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
3. HashMap的屬性
// 預設的初始容量,左移位4位相當於:1*2*2*2*2=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大的容量:2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 預設裝載因子為0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 當一個元素被新增到至少有8個節點的桶中,桶中的連結串列將會被轉化成紅黑樹,即轉化成紅黑樹條件是大於8個
static final int TREEIFY_THRESHOLD = 8;
// 紅黑樹退化成連結串列的條件:小於等於6時退化
static final int UNTREEIFY_THRESHOLD = 6;
// 轉化成紅黑樹的最小的桶的數量
static final int MIN_TREEIFY_CAPACITY = 64;
成員屬性有如下:
4. 構造方法
一共有4個構造方法:
其中,核心的構造方法是:
public HashMap(int initialCapacity, float loadFactor) {
// 保證初始容量大於等於0,否則丟擲異常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
// 保證初始容量不大於最大容量,超過了就講初始容量設定為最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 保證裝載因子大於0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
// 初始化裝載因子為0.75
// 當桶中元素到達8個的時候,概率已經變得非常小,也就是說用0.75作為載入因子,每個碰撞位置的連結串列長度超過8個是幾乎不可能的。當桶中元素到達8個的時候,概率已經變得非常小,也就是說用0.75作為載入因子,每個碰撞位置的連結串列長度超過8個是幾乎不可能的。
this.loadFactor = loadFactor;
// threshold這個成員變數是閾值,決定了是否要將雜湊表再雜湊,它的值應該是:capacity * load factor
// 但是這裡的threshold並不是真正的初始化閾值,正在的初始化閾值時在resize的時候進行初始化(而此時的threshold並不是沒有用,而是待會在初始化容量時候要用的初始值)
this.threshold = tableSizeFor(initialCapacity);
}
在初始化閾值容量的時候,呼叫了tableSizeFor
方法:
// 這個方法返回大於輸入數字的最近的2的整數次冪的數
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
5. put方法
put方法其實是呼叫了putVal方法的,呼叫方法的同時把計算好的key的雜湊值傳入,putVal方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put的過程如下:
Node<K,V>[] tab; // tab表示的是雜湊陣列
Node<K,V> p; // p表示的是陣列的第一個節點
Node<K,V> e; // e表示該key是否已經存在,為null表示不存在
-
put方法接收傳入key與value:
put(K key, V value)
-
計算出key的雜湊值,這裡計算的雜湊值方法是key的hashcode與hashcode的高16位進行異或運算得到的結果
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
-
將計算得到的雜湊值、key、value傳給putVal方法
-
在putVal方法中,先判斷雜湊陣列是否為空,如果為空的話就resize初始化tab,建立新的陣列
// 判斷tab是否為空 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
-
如果存在雜湊表,則計算key對應的索引位置:
p = tab[i = (n - 1) & hash
,使用length-1
與hash
進行邏輯與運算(因為在做&
運算的時候,僅僅是後4位有效,那麼如果key的雜湊值低位變化不大,高位變化大,那麼在計算的時候發生雜湊衝突的可能性也增大許多,所以上面在計算雜湊的時候將hash與hash的高16為進行異或運算得到結果作為雜湊值,增加了隨機性),如果改索引位置還沒有節點,那麼就直接插入到該位置即可!if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
-
如果該桶上有元素的話,就根據該桶的結構是紅黑樹還是連結串列進行插入,然後返回結果賦值給
e
:if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) // 是樹形結構按照樹形結構插入 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 按照連結串列結構插入 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 判斷是否要轉化成紅黑樹結構 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; } }
-
如果e是為
null
,就說明該key不存在,直接插入,如果不為null
,說明key已經存在,直接將覆蓋原來的value,並返回 -
插入成功之後,還要判斷一下實際存在的鍵值對的數量
size
是否大於閾值threshold
,如果大於那麼就擴容
6. 擴容
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) {
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;
@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;
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
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
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;
}
}
}
}
}
return newTab;
}
-
先判斷原來的容量是否大於0
-
如果大於0的話且大於等於最大容量,就將閾值設定為Integer.MAX_VALUE,然後啥也不幹
如果大於0的話且小於於最大容量就將舊的容量擴容為原來的兩倍,同時也將舊的閾值擴大為原來的兩倍
if (oldCap > 0) { 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 }
-
如果初始容量未制定或者小於等於0(就是HashMap構造方法的那種情況,只初始化了threshold閾值),那麼就將閾值作為初始化容量(此時閾值是2的整數次冪,HashMap的容量要為2的整數次冪)
else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr;
-
剩下的情況就是初始容量沒有設定,閾值也沒有設定,那麼容量就用預設的
DEFAULT_INITIAL_CAPACITY
,閾值則為:(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)
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;
-
最後就是將舊的資料複製到新陣列裡面,有兩種情況:
- 擴容後,若hash值新增參與運算的位=0,那麼元素在擴容後的位置=原始位置
- 擴容後,若hash值新增參與運算的位=1,那麼元素在擴容後的位置=原始位置+擴容後的舊位置
擴容後長度為原hash表的2倍,於是把hash表分為兩半,分為低位和高位,如果能把原連結串列的鍵值對, 一半放在低位,一半放在高位,而且是通過
e.hash & oldCap == 0
來判斷。因此有50%的概率放在新hash表低位,50%的概率放在新hash表高位。
7. get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
get方法的實現就是計算key的hash值,然後通過getNode獲取對應的value
8. remove方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
remove方法也是通過計算key的hash,呼叫removeNode來刪除元素的
9. HashMap的一些特性
- 允許key和value為null
- 除了允許為努力了和同步,其他的和HashTable一樣
- 不保證有序
- 初始容量太高或者太低對便利都不太好
- 當雜湊表容量超過初始容量*裝載因子時,雜湊表會進行再散裂,桶數量*2
- 不同步,想要同步可以使用Collections工具類實現
Map m = Collections.synchronizedMap(new HashMap(...));
- 裝載因子預設是0.75,設定高雖然會減少空間,但是遍歷的開銷會增加。因此在設定初始容量時,應該考慮好裝載因子和容量的大小,如果設定的好,就不用再散裂了