小編道行也沒那麼深,就用最通俗易懂的方式,來解釋hashmap實作原理。本文基于JDK8分析HashMap(),我們從源碼出發将主要分析讨論如下的幾個知識點:
- HashMap的特點是什麼?以及它的使用場景
- HashMap的資料結構?
- HashMap的工作原理是什麼?
- equals和hashCode都有什麼作用?
- 重寫equals()為什麼一定要重寫hashCode()?
- HashMap裡面的table數組為什麼是2的N次方?
1、感覺HashMap
我們首先進行如下操作:
HashMap<String, Integer> map = new HashMap<String, Integer>();
map.put("國文", 1);
map.put("數學", 2);
map.put("英語", 3);
map.put("曆史", 4);
map.put("政治", 5);
map.put("地理", 6);
map.put("生物", 7);
map.put("化學", 8);
for(Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
接着利用debug模式,從資料結構上認知HashMap,循序漸進

以下是JDK8中HashMap的資料結構源碼:
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
表,在首次使用時初始化,并根據需要調整大小。當配置設定時,長度總是2的幂。
*/
transient Node<K,V>[] table;
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
基本的哈希bin節點,用于大多數條目(内部類)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
2、HashMap的兩個重要參數
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
capacity就是初始化table時的數組容量,load factor指table的填充比例;當我們對疊代性能要求比較高時,我們首先不能把capacity設定的太大;同時load factor不要超過0.75,否則會明顯增加沖突幾率,降低HashMap性能
負載因子 * 容量 > 元素數量(put進去的元素個數)時,就需要調整容量(table的長度)為原來的2倍
3、HashMap的put(Key k,Value v)的原理
在分析源碼之前,我們先看看大體思路:
1)當在第一次put時,先對table初始化,通過hash計算得到存放位置table[i],存放。
2)當再次put時,同樣經過hash計算得到位置,則采用連結清單法解決沖突,存放在相同位置的next區域
3)在JDK8中設定了連結清單的預設門檻值為8,如果超過這個值,則進行樹化
4)如果節點已經存在就替換old value(保證key的唯一性)
5)如果bucket滿了(超過load factor*current capacity),就要resize,變為原來2倍
面試題:解釋HashMap的原理,資料量增大時,
在資料量小的時候,HashMap是按照連結清單的模式存儲的。當資料量變大之後,為了進行快速的查找,會将這個連結清單變成紅黑樹(均衡二叉樹),用hash碼作為資料的定位來進行儲存。
具體實作代碼如下:
public V put(K key, V value) {
// 對key的hashCode()做hash
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;
// tab為空則建立
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 計算index得出存放的位置,并對null做處理
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;
// 超過load factor*current capacity,resize()
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
4、get函數的實作
大緻思路如下:
- bucket裡的第一個節點,直接命中;
-
如果有沖突,則通過key.equals(k)去查找對應的entry
若為樹,則在樹中通過key.equals(k)查找,O(logn);
若為連結清單,則在連結清單中通過key.equals(k)查找,O(n)。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(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) {
// 在樹中get
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 在連結清單中get
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
為了避免篇幅過長,關于table的長度為什麼必須是2的N次方,hash函數的實作以及對開始問題的回答,在下篇部落格進行分析,請轉最新JDK8HashMap實作原理(二)
歡迎大家留言交流,小編q:1298364867