《Java1.8源碼分析》:HashMap
概要
HashMap 是一個關聯數組、哈希表,它是線程不安全的,允許key爲null,value爲null。遍歷時無序。
其底層數據結構是數組稱之爲哈希桶,每個桶裏面放的是鏈表,鏈表中的每個節點,就是哈希表中的每個元素。
在jdk8中,當鏈表長度爲8時,會轉爲紅黑樹,以提升它的查詢、插入效率。
因其底層哈希桶的數據結構是數組,所以也會涉及到擴容的問題。
當HashMap的容量達到threshold域值時,就會觸發擴容。擴容前後,哈希桶的長度一定會是2的次方。
這樣在根據key的hash值尋找對應的哈希桶時,可以用位運算替代取餘操作,更加高效。
而key的hash值,並不僅僅只是key對象的hashCode()方法的返回值,還會經過擾動函數的擾動,以使hash值更加均衡。
因爲hashCode()是int類型,取值範圍是40多億,只要哈希函數映射的比較均勻鬆散,碰撞機率是很小的。
但就算原本的hashCode()取得很好,每個key的hashCode()不同,但是由於HashMap的哈希桶的長度遠比hash取值範圍小,默認是16,所以當對hash值以桶的長度取餘,以找到存放該key的桶的下標時,由於取餘是通過與操作完成的,會忽略hash值的高位。因此只有hashCode()的低位參加運算,發生不同的hash值,但是得到的index相同的情況的機率會大大增加,這種情況稱之爲hash碰撞。 即,碰撞率會增大。
擾動函數就是爲了解決hash碰撞的。它會綜合hash值高位和低位的特徵,並存放在低位,因此在與運算時,相當於高低位一起參與了運算,以減少hash碰撞的概率。(在JDK8之前,擾動函數會擾動四次,JDK8簡化了這個操作)
擴容操作時,會new一個新的Node數組作爲哈希桶,然後將原哈希表中的所有數據(Node節點)移動到新的哈希桶中,相當於對原哈希表中所有的數據重新做了一個put操作。所以性能消耗很大,可想而知,在哈希表的容量越大時,性能消耗越明顯。
因爲擴容是容量翻倍,所以原鏈表上的每個節點,現在可能存放在原來的下標,即low位, 或者擴容後的下標,即high位。 high位= low位+原哈希桶容量
如果追加節點後,鏈表數量》=8,則轉化爲紅黑樹
繼承結構
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
HashMap繼承了AbstractMap及實現了Map、Cloneable和Serializable接口。
看過源碼的人可能都有這樣一個疑問:AbstractMap也實現了Map接口,爲什麼HashMap既繼承AbstractMap抽象類還需要實現Map接口嗎???
從功能上來說:HashMap實現Map是沒有任何作用的。
從結構上來說:由於我們一般是面對接口編程,爲了維護結構清晰和完整,是需要實現Map接口的。
而HashMap繼承AbstractMap的作用爲:AbstractMap 提供 Map 接口的骨幹實現,以最大限度地減少實現此接口所需的工作。
屬性
//HashMap初始的長度
1. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大長度2^31
2. static final int MAXIMUM_CAPACITY = 1 << 30;
//默認加載因子
3. static final float DEFAULT_LOAD_FACTOR = 0.75f;
//鏈表轉成紅黑樹的閾值
4. static final int TREEIFY_THRESHOLD = 8;
//紅黑樹轉爲鏈表的閾值
5. static final int UNTREEIFY_THRESHOLD = 6;
//存儲方式由鏈表轉成紅黑樹的容量的最小閾值
6. static final int MIN_TREEIFY_CAPACITY = 64;
//HashMap中存儲的鍵值對的數量
7. transient int size;
//擴容閾值,當size>=threshold時,就會擴容
8. int threshold;
//HashMap的加載因子
9. final float loadFactor;
這裏我們需要加載因子(load_factor),加載因子默認爲0.75,當HashMap中存儲的元素的數量大於(容量×加載因子),也就是默認大於16*0.75=12時,HashMap會進行擴容的操作。
構造函數
hashMap構造函數有4個
public HashMap() {
//默認構造函數,賦值加載因子爲默認的0.75f
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(int initialCapacity) {
//指定初始化容量的構造函數
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//同時指定初始化容量 以及 加載因子, 用的很少,一般不會修改loadFactor
public HashMap(int initialCapacity, float loadFactor) {
//邊界處理
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始容量最大不能超過2的30次方
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//顯然加載因子不能爲負數
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//設置閾值爲 》=初始化容量的 2的n次方的值
this.threshold = tableSizeFor(initialCapacity);
}
//新建一個哈希表,同時將另一個map m 裏的所有元素加入表中
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
//根據期望容量cap,返回2的n次方形式的 哈希桶的實際容量 length。 返回值一般會>=cap
static final int tableSizeFor(int cap) {
//經過下面的 或 和位移 運算, n最終各位都是1。
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
//判斷n是否越界,返回 2的n次方作爲 table(哈希桶)的閾值
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
//將另一個Map的所有元素加入表中,參數evict初始化時爲false,其他情況爲true
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//拿到m的元素數量
int s = m.size();
//如果數量大於0
if (s > 0) {
//如果當前表是空的
if (table == null) { // pre-size
//根據m的元素數量和當前表的加載因子,計算出閾值
float ft = ((float)s / loadFactor) + 1.0F;
//修正閾值的邊界 不能超過MAXIMUM_CAPACITY
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//如果新的閾值大於當前閾值
if (t > threshold)
//返回一個 》=新的閾值的 滿足2的n次方的閾值
threshold = tableSizeFor(t);
}
//如果當前元素表不是空的,但是 m的元素數量大於閾值,說明一定要擴容。
else if (s > threshold)
resize();
//遍歷 m 依次將元素加入當前表中。
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
擴容函數
final Node<K,V>[] resize() {
//oldTab 爲當前表的哈希桶
Node<K,V>[] oldTab = table;
//當前哈希桶的容量 length
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//當前的閾值
int oldThr = threshold;
//初始化新的容量和閾值爲0
int newCap, newThr = 0;
//如果當前容量大於0
if (oldCap > 0) {
//如果當前容量已經到達上限
if (oldCap >= MAXIMUM_CAPACITY) {
//則設置閾值是2的31次方-1
threshold = Integer.MAX_VALUE;
//同時返回當前的哈希桶,不再擴容
return oldTab;
}//否則新的容量爲舊的容量的兩倍。
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)//如果舊的容量大於等於默認初始容量16
//那麼新的閾值也等於舊的閾值的兩倍
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;//此時新表的容量爲默認的容量 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新的閾值爲默認容量16 * 默認加載因子0.75f = 12
}
if (newThr == 0) {//如果新的閾值是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];
//更新哈希桶引用
table = newTab;
//如果以前的哈希桶中有元素
//下面開始將當前哈希桶中的所有節點轉移到新的哈希桶中
if (oldTab != null) {
//遍歷老的哈希桶
for (int j = 0; j < oldCap; ++j) {
//取出當前的節點 e
Node<K,V> e;
//如果當前桶中有元素,則將鏈表賦值給e
if ((e = oldTab[j]) != null) {
//將原哈希桶置空以便GC
oldTab[j] = null;
//如果當前鏈表中就一個元素,(沒有發生哈希碰撞)
if (e.next == null)
//直接將這個元素放置在新的哈希桶裏。
//注意這裏取下標 是用 哈希值 與 桶的長度-1 。 由於桶的長度是2的n次方,這麼做其實是等於 一個模運算。但是效率更高
newTab[e.hash & (newCap - 1)] = e;
//如果發生過哈希碰撞 ,而且是節點數超過8個,轉化成了紅黑樹(暫且不談 避免過於複雜, 後續專門研究一下紅黑樹)
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果發生過哈希碰撞,節點數小於8個。則要根據鏈表上每個節點的哈希值,依次放入新哈希桶對應下標位置。
else { // preserve order
//因爲擴容是容量翻倍,所以原鏈表上的每個節點,現在可能存放在原來的下標,即low位, 或者擴容後的下標,即high位。 high位= low位+原哈希桶容量
//低位鏈表的頭結點、尾節點
Node<K,V> loHead = null, loTail = null;
//高位鏈表的頭節點、尾節點
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;//臨時節點 存放e的下一個節點
do {
next = e.next;
//這裏又是一個利用位運算 代替常規運算的高效點: 利用哈希值 與 舊的容量,可以得到哈希值去模後,是大於等於oldCap還是小於oldCap,等於0代表小於oldCap,應該存放在低位,否則存放在高位
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);
//將低位鏈表存放在原index處,
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//將高位鏈表存放在新index處
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
再看一下 往哈希表裏插入一個節點的putVal函數,如果參數onlyIfAbsent是true,那麼不會覆蓋相同key的值value。如果evict是false。那麼表示是在初始化時調用的
小結:
* 運算儘量都用位運算代替,更高效。
* 對於擴容導致需要新建數組存放更多元素時,除了要將老數組中的元素遷移過來,也記得將老數組中的引用置null,以便GC
* 取下標 是用 哈希值 與運算 (桶的長度-1) i = (n - 1) & hash。 由於桶的長度是2的n次方,這麼做其實是等於 一個模運算。但是效率更高
* 擴容時,如果發生過哈希碰撞,節點數小於8個。則要根據鏈表上每個節點的哈希值,依次放入新哈希桶對應下標位置。
* 因爲擴容是容量翻倍,所以原鏈表上的每個節點,現在可能存放在原來的下標,即low位, 或者擴容後的下標,即high位。 high位= low位+原哈希桶容量
* 利用哈希值 與運算 舊的容量 ,if ((e.hash & oldCap) == 0),可以得到哈希值去模後,是大於等於oldCap還是小於oldCap,等於0代表小於oldCap,應該存放在低位,否則存放在高位。這裏又是一個利用位運算 代替常規運算的高效點
* 如果追加節點後,鏈表數量》=8,則轉化爲紅黑樹
* 插入節點操作時,有一些空實現的函數,用作LinkedHashMap重寫使用。
put函數
public V put(K key, V value) {
//先根據key,取得hash值。 再調用上一節的方法插入節點
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
remove函數
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
// p是待刪除節點的前置節點
Node<K,V>[] tab; Node<K,V> p; int n, index;
//如果哈希表不爲空,則根據hash值算出的index下 有節點的話。
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//node是待刪除節點
Node<K,V> node = null, e; K k; V v;
//如果鏈表頭的就是需要刪除的節點
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;//將待刪除節點引用賦給node
else if ((e = p.next) != null) {//否則循環遍歷 找到待刪除節點,賦值給node
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//如果有待刪除節點node, 且 matchValue爲false,或者值也相等
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)//如果node == p,說明是鏈表頭是待刪除節點
tab[index] = node.next;
else//否則待刪除節點在表中間
p.next = node.next;
++modCount;//修改modCount
--size;//修改size
afterNodeRemoval(node);//LinkedHashMap回調函數
return node;
}
}
return null;
}
get函數
public V get(Object key) {
Node<K,V> e;
//傳入擾動後的哈希值 和 key 找到目標節點Node
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//傳入擾動後的哈希值 和 key 找到目標節點Node
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//查找過程和刪除基本差不多, 找到返回節點,否則返回null
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;
}
與HashTable的區別
- 與之相比HashTable是線程安全的,且不允許key、value是null。 HashTable默認容量是11。
- HashTable是直接使用key的hashCode(key.hashCode())作爲hash值,不像HashMap內部使用static
- final int hash(Object key)擾動函數對key的hashCode進行擾動後作爲hash值。
- HashTable取哈希桶下標是直接用模運算%.(因爲其默認容量也不是2的n次方。所以也無法用位運算替代模運算)
- 擴容時,新容量是原來的2倍+1。int newCapacity = (oldCapacity << 1) + 1;
- Hashtable是Dictionary的子類同時也實現了Map接口,HashMap是Map接口的一個實現類;
引用
https://blog.csdn.net/zxt0601/article/details/77413921
https://blog.csdn.net/u010412719/article/details/51980632