看到網上對HashMap源碼分析的文章很多,大部分概念都是對的,但是沒有讓人理解哈希表的本質,今天畫了一些時間認真的看了一遍HashMap的源碼,所以想寫下這篇文章總結一下。
先來一張HashMap的底層數據結構圖:
這張圖大家是很熟悉的,HashMap底層就是一個Node<K, V> [] table,源碼如下:
//用來存key-value對象
transient Node<K,V>[] table;
//其中Node<key, value>是HashMap的一個靜態內部類
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...
}
HashMap中有幾個比較關鍵的常量需要我們瞭解一下:
//默認的初始化大小,也就是Node<K,V>[]的默認長度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//集合允許存放的元素最大個數
static final int MAXIMUM_CAPACITY = 1 << 30;
//默認裝載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//當單條鏈表的長度大於等8並且容量大於64時,就會將鏈表轉換成紅黑樹
static final int TREEIFY_THRESHOLD = 8;
//當單條鏈表的長度小於等於6時,就會將紅黑樹轉換成鏈表
static final int UNTREEIFY_THRESHOLD = 6;
//當單條鏈表的長度大於等8並且容量大於64時,就會將鏈表轉換成紅黑樹
static final int MIN_TREEIFY_CAPACITY = 64;
//hash表的元素個數
transient int size;
//當size大於等於這個數時會進行rehash
int threshold;
//負載因子,如果沒有傳入則使用默認值 0.75f
final float loadFactor;
//記錄hash表的修改次數
transient int modCount;
一、HashMap的構造函數
HashMap提供了無參構造函數和幾個重載的有參構造函數,裏面做的事情都沒啥區別,就是給loadFactor和threshold賦初始值
源碼:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
/**
* tableSizeFor方法用來將輸入的值轉換成2的整數倍,假如你輸入的初始大小爲7,則會
* 幫你自動轉換成8,因爲HashMap中table的長度永遠是2的整數倍
*/
this.threshold = tableSizeFor(initialCapacity);
}
這裏的threshold 並不是最終的用來判斷是否需要resize的值,而是table的長度,此時的table也是null,在向HashMap中放入第一個key-value時,會初始化table,並重新計算threshold。
二:HashMap的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)
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;
}
總結起來就是以下幾步:
- 判斷table是否爲空,如果爲空則進行resize,rezise時會重新設置threshold的值;
- 判斷hash值對應的位置是否有元素,如果沒有則直接放在對應的位置;
- 如果已經有元素了則進行判斷:
- 如果第一個元素的key等於要加入的key,則直接e標記爲第一個元素
- 如果第一個元素是一個紅黑樹,則調用紅黑樹的put方法
- 對鏈表進行遍歷,如果key匹配,則將e標記成這個元素,否則將新插入的元素放在鏈表的末尾,注意不是表頭:
- 判斷鏈表的長度是否大於等於TREEIFY_THRESHOLD,如果是,則將鏈表轉換成紅黑樹
- 如果e不等於空,則將e對應的value更新,並將oldvalue返回
- 記錄hashMap的操作次數,判斷size如果大於負載因子,則進行resize。
基本上看懂了put的過程,get的過程就很簡單了,自己去看一下原碼就明白了。
再額外說一下爲什麼HashMap中table的長度要設置成2的整數倍,因爲我們是通過key的hash值來確定key對應的數組位置的,那麼如果對應了,我們肯定想到了取模,例如:table的長度是16,則對16取模就可以了,也就是hashcode % 16,但是取模效率是很低的,其實對於2的整數倍對任何數取模可以直接用&操作,上面的例子就可以改爲 hashcode & (16 - 1)。總結成公式就是hashcode % length = hashcode & (length - 1),這裏的&操作效率可比%高得多。
PS:負載因子默認是0.75,所以Map中的元素個數不會達到初始化的容量就會進行resize,我們在初始化HashMap,給定容量大小時一定要考慮這一點。