關於HashMap要知道的事
HashMap資料結構
底層使用了陣列+連結串列。
如果,連結串列的長度大於等於8(TREEIFY_THRESHOLD)了,則將連結串列改為紅黑樹,這是Java8 的一個新的優化。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
當發生 雜湊衝突(碰撞)的時候,HashMap 採用 拉鍊法 進行解決。
擴容開銷很大(需要建立新陣列、重新雜湊、分配等等),與擴容相關的兩個因素:
預設載入因子(DEFAULT_LOAD_FACTOR)是 0.75。HashMap 使用此值基本是平衡了效能和空間的取捨。
- 載入因子太大的話發生衝突的可能就會大,查詢的效率反而變低
- 太小的話頻繁 rehash,導致效能降低
載入因子決定了 HashMap 中的元素佔有多少比例時擴容
初識容量(DEFAULT_INITIAL_CAPACITY)16。
HashMap擴容的時機:
容器中的元素數量 > 負載因此 * 容量,如果負載因子是0.75,容量是16,那麼當容器中數量達到13 的時候就會擴容。
還有,如果某個連結串列長度達到了8,並且容量小於64(MIN_TREEIFY_CAPACITY),則也會用擴容代替紅黑樹。
/** * Replaces all linked nodes in bin at index for given hash unless * table is too small, in which case resizes instead. */ final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
HashMap中的效能優化
HashMap 擴容的時候,不管是連結串列還是紅黑樹,都會對這些資料進行重新的雜湊計算,然後縮短他們的長度,優化效能。在進行雜湊計算的時候,會進一步優化效能,減少減一的操作,直接使用& 運算。
HashMap的重新Hash演算法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
避免只靠低位資料來計算雜湊時導致的衝突,計算結果由高低位結合決定,可以避免雜湊值分佈不均勻。
而且,採用位運算效率更高。
HashMap 如何根據 hash 值找到陣列中的物件
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) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
關鍵的是其中的這一行:first = tab[(n - 1) & hash])
當n是2的指數時,上面的(n-1)&hash相當於hash%n,對於處理器來說,除法和求餘比較慢,為了效能,使用了減法和按位與運算。
如何正確使用
無論我們如何設定初始容量,HashMap 都會將我們改成2的冪次方,也就是說,HashMap 的容量百分之百是 2的冪次方。
但是,請注意:如果我們預計插入7條資料,那麼我們寫入7,HashMap 會設定為 8,雖然是2的冪次方,但是,請注意,當我們放入第7條資料的時候,就會引起擴容,造成效能損失,所以,知曉了原理,我們以後在設定容量的時候還是自己算一下,比如放7條資料,我們還是都是設定成16,這樣就不會擴容了。
非執行緒安全
HashMap 在 JDK 7 中併發擴容的時候是非常危險的,非常容易導致連結串列成環狀。但 JDK 8 中已經修改了此bug。但還是不建議使用。強烈推薦併發容器 ConcurrentHashMap。
編碼啟示
如果參與中介軟體、基礎架構開發,時刻追求效能是很有必要的。
HashMap如何根據指定容量設定閾值
得出最接近指定引數 cap 的 2 的 N 次方容量。假如你傳入的是 5,返回的初始容量為 8 。
/**
* 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;
}
可以自己舉例驗證,但是如果問怎麼寫出來的,我也不知道。只能膜拜。
二進位制位運算規則參考:
<< : 左移運算子,num << 1,相當於num乘以2 低位補0
>> : 右移運算子,num >> 1,相當於num除以2 高位補0
>>> : 無符號右移,忽略符號位,空位都以0補齊
% : 模運算 取餘
^ : 位異或 第一個運算元的第n位與第二個運算元的第n位相反,那麼結果的第n為也為1,否則為0
& : 與運算 第一個運算元的第n位與第二個運算元的第n位如果都是1,那麼結果的第n為也為1,否則為0
| : 或運算 第一個運算元的第n位與第二個運算元的第n位 只要有一個是1,那麼結果的第n為也為1,否則為0
~ : 非運算 運算元的第n位為1,那麼結果的第n位為0,反之,也就是取反運算(一元操作符:只操作一個數)
參考文章:
深入理解-HashMap-put-方法(JDK-8逐行剖析)
Java 集合深入理解(16):HashMap 主要特點和關鍵方法原始碼解讀
Java原始碼分析:關於 HashMap 1.8 的重大更新