Hash散列基本思想
哈希表使用數組和鏈表共同實現散列存儲,每一個數組元素可以認爲是散列表中的桶位(buket),每個桶位存放一個鏈表,該鏈表由散列碼(hashCode)相同的節點構成。Hash表的查找就是根據需要查找的對象(key, value)中的key,利用散列函數計算key對應的hashCode,即數組的下標(buket的索引),在O(1)時間內找到對應的桶位,再遍歷該桶位內的鏈表,查找對應的value值即可。
在JDK8中,當桶位數目過多(默認至少64)或者某一個桶位的鏈表長度過長時(默認是8),查找效率會顯著降低。因此HashMap會將鏈表的普通節點轉化爲樹節點(TreeNode)存儲,鏈表List也將轉爲Tree樹將搜索效率提升到O(logn),但是TreeNode的空間消耗是普通Node空間消耗的兩倍,在HashMap進行多次remove操作之後,如果桶位數目和鏈表長度低於閾值,TreeNode重新轉化爲Node,Tree樹轉爲List鏈表。
Hash表長度爲2n 與查找效率
Hash表中的table數組存放node,table的長度size必須爲2的冪,在這個前提下有如下規律:對任意一個哈希碼hashCode利用求餘運算進行散列,即index=hashCode%size時,index爲hashCode所在的數組桶位下標,由於求餘取模運算效率低下,在size爲2的冪的前提下,可以用位與運算代替,即index=hashCode & (size – 1),得到的是相同的結果。(參見博文當length爲2^n, m & (length-1) 相當於 m % length 的證明)
下面是保證輸入一個表的初始長度,是table的size總是2的冪:
/**
* Returns a power of two size for the given target capacity.
*/
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;
}
重點函數實現
初始容量和負載因子
在HashMap中有兩個很重要的參數,容量(Capacity)和負載因子(Load factor):
- Initial capacity The capacity is the number of buckets in the hash table, The initial capacity is simply the capacity at the time the hash table is created.
- Load factor The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased.
簡單的說,Capacity就是bucket的大小,Load factor就是bucket填滿程度的最大比例。如果對迭代性能要求很高的話不要把capacity設置過大,也不要把load factor設置過小。當bucket中的entries的數目大於capacity*load factor時就需要調整bucket的大小爲當前的2倍。
put函數的實現
- 對key的hashCode()做hash,然後再計算index;
- 如果沒碰撞直接放到bucket裏;
- 如果碰撞了,以鏈表的形式存在buckets後;
- 如果碰撞導致鏈表過長(大於等於TREEIFY_THRESHOLD),就把鏈表轉換成紅黑樹;
- 如果節點已經存在就替換old value(保證key的唯一性;
- 如果bucket滿了(超過load factor*current capacity),就要resize。
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;
}
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;
}
hash()、resize()
強烈推薦閱讀Java HashMap工作原理及實現,裏面有詳細的介紹。
相關問題
什麼時候會使用HashMap?他有什麼特點?
是基於Map接口的實現,存儲鍵值對時,它可以接收null的鍵值,是非同步的,HashMap存儲着Entry(hash, key, value, next)對象。你知道HashMap的工作原理嗎?
通過hash的方法,通過put和get存儲和獲取對象。存儲對象時,我們將K/V傳給put方法時,它調用hashCode計算hash從而得到bucket位置,進一步存儲,HashMap會根據當前bucket的佔用情況自動調整容量(超過Load Facotr則resize爲原來的2倍)。獲取對象時,我們將K傳給get,它調用hashCode計算hash從而得到bucket位置,並進一步調用equals()方法確定鍵值對。如果發生碰撞的時候,Hashmap通過鏈表將產生碰撞衝突的元素組織起來,在Java 8中,如果一個bucket中碰撞衝突的元素超過某個限制(默認是8),則使用紅黑樹來替換鏈表,從而提高速度。你知道get和put的原理嗎?equals()和hashCode()的都有什麼作用?
通過對key的hashCode()進行hashing,並計算下標( n-1 & hash),從而獲得buckets的位置。如果產生碰撞,則利用key.equals()方法去鏈表或樹中去查找對應的節點你知道hash的實現嗎?爲什麼要這樣實現?
在Java 1.8的實現中,是通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這麼做可以在bucket的n比較小的時候,也能保證考慮到高低bit都參與到hash的計算中,同時不會有太大的開銷。如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?
如果超過了負載因子(默認0.75),則會重新resize一個原來長度兩倍的HashMap,並且重新調用hash方法。