小編道行也沒那麼深,就用最通俗易懂的方式,來解釋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的兩個重要參數
/**
* The default initial capacity - MUST be a power of two.table的默認初始容量
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16/**
* The load factor used when none specified in constructor.(負載因子)
*/
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