1.構造函數如下
HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)
HashMap(Map<? extends K,? extends V> m)
2.底層實現思想
(1) 基於數組和鏈表實現,拉鍊法,數組存在擴容不存在縮容,鏈表在java8里加入了紅黑樹結構,見下文
(2) 在通過迭代器遍歷HashMap的Node過程中,如果進行了結構性的更改,會fail-fast快失敗,會拋出ConcurrentModificationException,api doc講到依賴這個異常去糾錯是不合理的,而是應該僅僅是去發現bugs
一、常見問題解答(大多是api doc上的原話或是看源碼自己的總結)
1.擴容機制
翻倍擴容,容量變成原來的兩倍,默認容量爲16,也可初始指定容量,初始化的時候,閾值threshold爲capacity,也就是說第一次threshold並不是等於capacity*loadFactor,當元素個數到達threshold時會擴容,即resize
2.負載因子爲什麼默認取0.75
負載因子高,空間利用率高,但是查詢效率低,容易哈希碰撞。負載因子低,空間利用低,浪費空間。因此折中
另外,只有哈希碰撞嚴重時,纔會出現紅黑樹結構,紅黑樹節點佔用空間大約是普通節點2倍,實際上很少出現
理想情況隨機哈希下,節點bucket上出現node個數和概率滿足泊松分佈,當負載因子是0.75時,泊松分佈的lambda值是0.5,每個bucket鏈表節點node出現的個數和概率滿足一個公式(見api doc)
當出現8-9個node時的概率,算法理想上只有千萬分之1,即 1 in ten million
3.哈希函數是怎麼設計的,哈希是怎麼定址的
使用key.hashCode的高16位保持不變,低16位爲高16位和低16位的異或,即(h = key.hashCode()) ^ (h >>> 16)
原因:通常的hashCode函數已經足夠合理分佈了,我們沒有必要打亂他的節奏,考慮到位運算的便捷和快速,減少系統損耗,容量又是2的冪,哈希值只是比特位不一樣,因此我們用高位和低位異或,加入對高位的影響
而哈希碰撞用紅黑樹處理。哈希定址採用哈希值對容量取模,源碼中是通過與capacity-1進行位運算,因爲位運算快
4.如果哈希碰撞嚴重,可能有哪些原因
可能是重寫Key的哈希函數設計的不合理,儘量用Objects.hash即可,系統自帶的。再就可能是負載因子設置的過高
5.鏈表和紅黑樹轉換的規則是什麼樣的
每個桶上鍊表元素個數>=8個元素則轉換爲紅黑樹存儲(並且滿足capacity>=64),減少到<=6個又變回鏈表結構(擴容時,可能一條鏈變兩條鏈,所以元素個數會減少),刪除結點時,變回鏈表的觸發條件因樹的結構而異,此時樹大概只有2-6個node,源碼中的條件是root的左兒子的左兒子爲空,具體源碼註釋有講到
6.java7的HashMap和java8的HashMap有哪些區別
java8源碼就增加了幾千行,加入了很多默認函數,lambda等,更重要的是java7插入節點使用頭插法(會產生環形鏈表死循環問題)和java8使用尾插法,哈希定址計算方式也不一樣,並且java8引入了紅黑樹,這是java7不具備的。
7.java8的HashMap爲什麼也不是線程安全的
resize函數就是不安全的,還沒複製完,另一個線程訪問,此時table部分bin爲null,源碼中的處理是先開闢新數組,再複製元素。
put的時候也會出現問題,bin有值,然而讀不到,本該形成鏈表,結果覆蓋了另一個線程新put的值
size變量也不是volatile線程可見的,而且有++size操作
8.HashMap裏結點Node的結構是什麼樣的
Map.Entry是Map接口裏的public內部子接口
HashMap.Node是普通鏈表節點,是內部類,它實現了Map.Entry
HashMap.TreeNode是紅黑樹節點,是final內部類,它繼承了LinkedHashMap.Entry,而後者又繼承了HashMap.Node
9.Hashtable和HashMap的區別
一個區別是線程安全性,一個是key和value是否可以爲null
10.HashMap.Node的hash屬性和key屬性爲什麼是final的
因爲不能改變,hash不用重複計算,節約計算代價
11.紅黑樹的排序規則是怎麼樣的
紅黑樹的排序首先使用hash值比較,其次是Key的compareTo方法(判斷實現Comparable接口),再是類名字符串等(如通過反射)和identityHashCode值排序,構造紅黑樹
二、源碼解析
1.哈希計算方法
/**
* 哈希計算規則
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2.容量capacity計算規則
/**
* 打成2的冪,這就是位運算的魅力
*/
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;
}
3.put方法底層原理
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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)
//該bin是空,直接添加,注意哈希定址是位運算,i = (n - 1) & hash
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))))
//key已存在,直接找到,就替換
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;
}
4.擴容函數
/**
* 擴容機制
* @return the table
*/
final Node<K,V>[] resize() {
//舊數組
Node<K,V>[] oldTab = table;
//舊容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//舊閾值
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//位運算capacity進行翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//開闢新數組
//記住,此時數組替換了,node元素還沒過來
table = newTab;
if (oldTab != null) {
//開始把舊數組的node元素複製到新數組中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//清理內存
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//紅黑樹結構的擴容
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order保持秩序順序
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//敲黑板,這裏只判斷最高位,如果不爲0,那麼hash值大於舊的容量,要放到高位的鏈表中,
//這就是擴容爲什麼一條鏈表可能變2條的原因
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
//魅力之處,直接+oldCap定址,這是與java7的一個不同點
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回新開闢的數組
return newTab;
}
5.把hashmap對應哈希值位置的bucket變成紅黑樹
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);
//最後調用頭結點的treeify方法,將其轉變爲具備父子關係的紅黑樹結構
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
三、紅黑樹結點內部的方法
1.把樹的root放到鏈表的頭
/**
* 把root放到鏈表的頭部去
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
Node<K,V> rn;
tab[index] = root;
TreeNode<K,V> rp = root.prev;
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
assert checkInvariants(root);
}
}
2.紅黑樹的二叉查找
/**
* 從當前TreeNode往子孫節點搜索k
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
//紅黑樹是二叉有序的,二分搜索,log(n)複雜度
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
//首先比較哈希值,
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
//直接找到
return p;
else if (pl == null)
//如果左邊null,則到右邊搜
p = pr;
else if (pr == null)
//如果右邊null,則到左邊搜
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
//如果能通過compareTo判斷,則這樣繼續搜索
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
//否則類似遞歸,繼續find從右兒子搜索,直到找到
return q;
else
//如果右兒子搜索沒搜到,繼續從左兒子往下搜
p = pl;
} while (p != null);
return null;
}
3.建立紅黑樹,key怎麼比較大小
//首先通過哈希值比較,然後通過key的compareTo方法,如果都無法比較大小,那麼採用下面的方法比較大小
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
//如果通過類名字符串無法區分,用identityHashCode應該能區分吧,這是內存級別的
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
4.把紅黑樹打成鏈表
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
//把樹節點轉換爲普通鏈表節點,然後next串起來,prev之前有值不變
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
//返回頭節點
return hd;
}
//真的佩服源碼的規範性,一般t代表temp臨時變量,l代表左邊,r代表右邊,h代表head或high
//還有比如,hd代表head,tail代表尾巴,tl代表temp臨時變量,等等
5.紅黑樹刪除節點(不包含顏色調整),先看圖解,如果不懂原理請後續關注我的紅黑樹基礎-第二篇
這個函數主要是this是要刪除的節點,找到節點s與之互換,然後用balanceDeletion調整,看下圖中樹結構的變化再看代碼註釋
/**
* Removes the given node, that must be present before this call.
* This is messier than typical red-black deletion code because we
* cannot swap the contents of an interior node with a leaf
* successor that is pinned by "next" pointers that are accessible
* independently during traversal. So instead we swap the tree
* linkages. If the current tree appears to have too few nodes,
* the bin is converted back to a plain bin. (The test triggers
* somewhere between 2 and 6 nodes, depending on tree structure).
* @param map 該hashmap
* @param tab hashmap內部Node數組
* @param movable 是否需要移動root到鏈表頭
* @param this 待刪除節點p
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash;
//root是根節點
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
//分別是this=p的鏈表指針的前驅和後繼
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
if (pred == null)
//如果要刪除的p是鏈表的頭,那麼first = succ;並且tab[index] = succ;
tab[index] = first = succ;
else
//否則鏈表斷開this的鏈接
pred.next = succ;
if (succ != null)
//鏈表斷開this的鏈接,把鏈表關係完善
succ.prev = pred;
if (first == null)
//空樹
return;
if (root.parent != null)
//獲取到真正的根root
root = root.root();
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
//根的左孩子的左孩子爲空,基本上可以判斷只剩2-6個node了,紅黑樹可以變成鏈表了
tab[index] = first.untreeify(map); // too small
return;
}
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
//主要關注這種左右孩子都非空的場景,爲什麼這種這麼複雜,請看我寫的關於紅黑樹基礎的其他博客
if (pl != null && pr != null) {
//請看圖解,我隨便畫了個圖,節點名和變量名一致
TreeNode<K,V> s = pr, sl;
//先找到大於刪除節點p的最小節點,爲什麼這麼做,請看紅黑樹基礎-第3篇
while ((sl = s.left) != null) // find successor
s = sl;
//首先互換p節點和s節點的顏色,因爲最終s要被換到p的位置,p要被換到s的位置
//互換後,在p位置的s因爲是p的顏色,所以不影響紅黑樹的性質
//而換到s位置的p,顏色是原s節點的顏色
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
if (s == pr) { // p was s's direct parent
//此時s==sp==pr,其實是建立p和s的關係,將else的內容簡化了
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
//建立p和sp的新父子關係,大家可以畫圖分析
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
//建立s和pr的新父子關係
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
//建立p和sr的新父子關係
if ((p.right = sr) != null)
sr.parent = p;
//建立s和pl的新父子關係
if ((s.left = pl) != null)
pl.parent = s;
//建立s和pp的新父子關係
//如果pp爲空,之前p就是根節點,那麼現在s就是根節點了
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
//如果原來p是pp的左孩子,互換後s就還是pp左孩子
pp.left = s;
else
pp.right = s;
//如果sr不爲空,則互換後p有右孩子,沒有左孩子,
if (sr != null)
//單鏈接情況直接用孩子替換
replacement = sr;
else
//此時p沒有孩子
replacement = p;
}
//單鏈接情況,單單隻有左孩子
else if (pl != null)
replacement = pl;
//單鏈接情況,單單隻有右孩子
else if (pr != null)
replacement = pr;
else
//p是葉子結點
replacement = p;
//互換後,如果p不是葉子結點,在樹結構中直接把p節點幹掉
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
//回到紅黑樹的平衡刪除了,如果要刪除的節點是紅色,那麼直接刪除即可
//如果p是黑色的,那麼此時就不滿足紅黑樹的性質了,因爲少了一個黑色節點,那麼要進行balanceDeletion調整
//注意:p的顏色是原來s的顏色
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
//那麼如果p是葉子結點,在樹結構中直接把p節點幹掉,detach斷開連接
if (replacement == p) { // detach
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
//是否需要把root放到鏈表head
if (movable)
moveRootToFront(tab, r);
}
6.其他部分函數未完待續
紅黑樹源碼部分(左右旋轉,平衡插入和平衡刪除請看我第三篇紅黑樹源碼解析)