HashMap工作原理回顧 (基於JDK1.8源碼分析)
空的HashMap()構造方法
/**
* 默認初始容量爲16.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量爲2的30次方,如果有參構造器指定了更大的數值,那麼仍然以2的30次方爲準
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默認加載因子,用來在擴容的時候使用
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 空參構造器,使用初始容量16和默認加載因子0.75
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
put(K key, V value) 方法
/**
* put方法源代碼
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
- 我們可以看到方法內部實際調用了一個putVal方法,hash(key)是對key取hash值。
/**
* putVal源代碼
* onlyIfAbsent參數:如果爲true,則不改變已存在的value。
* evict參數:該參數在HashMap中並沒有具體使用到,暫且忽略。
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果table爲空或長度爲0, 則初始化一個初始容量和默認閾值的Node數組
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 經過(n-1)&hash這樣的邏輯與運算得到數組下標,在該數組下標位置的元素如果爲空,則根據參數構造一個Node放入該下標位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 如果數組下標位置的Node不爲空
else {
Node<K,V> e; K k;
// 如果下標位置的節點的hash值與給定的hash值相同且key相同,那麼爲同一個Node,替換之。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果Node爲樹節點的處理
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 如果下標位置的節點hash值以及key值對比不一樣(其中包括hash值相同而key值不同的情況,也就是我們常遇到的hash衝突,應如下處理)
else {
for (int binCount = 0; ; ++binCount) {
// 如果下標位置的節點next節點爲空,則指定該下標節點的next節點爲新節點,跳出循環
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果下標位置的節點next節點不爲空且hash值及key相同,直接跳出循環。不然替換爲自身。
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;
// 如果size超過閾值(如16*0.75=12),則調用resize方法進行擴容且以2的N次冪進行擴容。擴容過程中會對數組內節點的位置進行重新計算從而進行變動。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
get(K key) 方法
/**
* 方法源碼
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
- 我們可以看到getNode方法,我們進入此方法看看其具體實現。
/**
* 此處的hash值與put方法中的hash值計算算法是一樣的,有興趣可以自己看下源碼
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 如果節點數組table不爲空且長度大於0,經過(n-1)&hash這樣的邏輯與運算得到下標後,在節點數組table中該下標位置的節點如果不爲空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果該下標位置的節點hash值與參數hash值相等且key相同,則返回該節點
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果該下標位置的節點hash值與參數hash值不相等或者key不相同,且該節點的next節點不爲空
if ((e = first.next) != null) {
// 處理如果是樹節點的情況
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 不斷循環查找next節點,直到找到匹配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;
}
remove(K key) 方法
/**
* 方法源碼
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
- 同樣的,我們可以看到一個removeNode方法,我們再看看這個方法的具體實現。
/**
* 此處的hash也是和put方法中的hash值運算算法相同得到的,
* matchValue指是否需要匹配value值,movable指是否需要移除。
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 如果Node數組table不爲空且長度大於0,經過(n-1)&hash這樣邏輯與運算得到數組下標後,在數組該下標位置的Node不爲空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 該下標位置的節點hash值與指定hash值相等且key相同,則匹配
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 若下標位置的節點hash值與指定hash值不相等或key不相同
else if ((e = p.next) != null) {
// 處理樹節點的情況
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// 循環查找next節點直到找到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);
}
}
// 如果匹配到hash值以及key值都相同的節點,且不需要匹配value或者value匹配也相等
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);
// 如果node節點與數組下標位置節點是同一個節點,那麼將下標位置指向node的next節點,這樣下標位置的原節點就不存在了。
else if (node == p)
tab[index] = node.next;
// 如果node節點與下標位置節點不是同一個節點,而是其鏈表中的其它節點(循環匹配到的其中一個next節點),那麼就循環後p節點(看循環代碼處實際是原來的e節點)的next節點指向node節點(看循環代碼處實際是新的e節點)的next節點。
else
p.next = node.next;
// 修改變動變量加一,容量減一。
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
至此,HashMap的三個常用的基本方法源碼分析就介紹到這裏,歡迎各位同僚提出批評與建議,轉載請說明出處,謝謝!