jdk原始碼閱讀之HashMap(一)
HashMap是一個散列表,這個散列表包含的雜湊桶的格式為2^n個,而且必須滿足這個條件,原因在getNode(int hash, Object key)
這個方法的分析中給出。HashMap允許null的鍵值對,null的鍵值對總會被對映到第一個雜湊桶中。HashMap裡面的鍵值對並沒有順序之分,裡面的結構是使用陣列+連結串列或者是紅黑樹,是否將連結串列變成紅黑樹的條件是,一個桶裡面節點的個數大於MIN_TREEIFY_CAPACITY
static final int MIN_TREEIFY_CAPACITY = 64;
HashMap的容量是指雜湊桶的個數,在建構函式中傳入,如果構造時傳入的引數不滿足這個條件,內部會做相應的調整,以滿足,HashMap的建構函式如下:
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
預設的HashMap大小為16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
HashMap是一個散列表,散列表的節點是Node這個類
//Node實現了Map類裡面的Entry介面 static class Node<K,V> implements Map.Entry<K,V> { final int hash; //節點的雜湊碼 final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } /* 節點的雜湊碼,注意這個並不是用來定位key的位置的雜湊碼方法, * 獲得key的雜湊碼的方法是HashMap.hash方法,使用這個雜湊碼來決 * 定該key存放在哪個雜湊桶內 */ public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { //相等的依據是兩者引用相等,或者兩者的key、value分別相等 if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
每個節點的雜湊碼是以key的雜湊碼來計算的,具體計算方式如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
從上面的雜湊碼計算方式可知
1.如果key為null,則雜湊碼為0,因為HashMap的鍵值對可以為null-null,因此,key為null的節點都會被對映到第一個雜湊桶裡面。
2.如果key不為null,則呼叫key的雜湊碼生成方法,則返回key本身的雜湊碼異或高16位,這樣做的原因是當散列表比較小的時候,讓高位一塊進行運算,更能有效的防止大多數節點對映到同一個雜湊桶.
解決了雜湊碼的生成之後,如何將雜湊碼和儲存位置聯絡起來呢?讓我們先看一下獲取節點的方法
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代表的是雜湊桶的第一個節點,從獲取節點的方式可以知道,雜湊桶的位置
其實就是節點的雜湊碼位與雜湊桶個數-1,**(注意一個桶裡面的key的雜湊碼並不一樣)**,為什麼要與上雜湊桶個數-1,原因在於
雜湊桶的個數為2^n,即1<<n,(1<<n)-1可以保證低位全部為1,這樣的話,也不會
造成陣列的越界,我想這也應該是雜湊桶個數為什麼必須是2^n個的原因
*/
(first = tab[(n - 1) & hash]) != null) {
//首先檢查第一個節點是否滿足要求,碰到一個key相等的,即返回這個節點
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 {
//遍歷這個雜湊桶,知道獲取到正確的節點,否則返回null
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
put操作
put操作是向HashMap新增一個鍵值對,如果該鍵已經存在,則更新舊的值
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 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;
//如果HashMap裡面還沒有任何鍵值對,則進行resize操作進行擴容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果該鍵值對對映的雜湊桶還沒有任何元素,則直接對映到該雜湊桶
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//雜湊桶存在元素
Node<K,V> e; K k;
//檢查第一個,如果第一個節點的key已經和需要插入的key相同,則e=p
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//如果p為紅黑樹的節點,這裡是為了提高查詢效能,當節點衝突太高的時候,使用紅黑樹而不是連結串列,執行完這一句,e仍然指向需要更新的節點或者null.
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//既不是雜湊桶的第一個,又不是紅黑樹,則遍歷連結串列
//下面的for迴圈只是象徵性的,因為不知道連結串列裡面元素的個數,當滿足一定條件就break
for (int binCount = 0; ; ++binCount) {
//已經遍歷到連結串列尾,說明沒有相同的key存在,則插入新的節點
if ((e = p.next) == null) {
//插入新的節點,這時,e引用的是新節點
p.next = newNode(hash, key, value, null);
//將連結串列轉化成紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果找到相同的key節點,e引用這個節點
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果執行到這裡,那麼e已經是引用了新的節點,或者舊的相同key的節點
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;
}
resize操作
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//oldTab為null的時候,雜湊桶裡面沒有任何元素
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;
}
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
//舊的容量為0
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
//threshold=capacity*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;
//如果舊的table不為空,則重新雜湊
if (oldTab != null) {
//遍歷所有的雜湊桶
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;
//如果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;
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;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
對於上面舊雜湊桶中元素重新雜湊的部分,進行說明,因為程式碼比較難懂,首先要宣告的是舊的一個雜湊桶中的元素重新雜湊的可能只有兩種:
假如舊雜湊桶個數oldCap為2^5=32個,那麼table的定址範圍為:0~31,對第20個雜湊桶中的元素進行分析,雜湊碼位h,那麼先來看h的可能取值:
h^(32-1)=20 二進位制表示:h ^ 11111=10100
可以得出h的可能:xxx10100
現在重新雜湊,雜湊桶個數翻倍newCap=2^6=64,雜湊桶的位置:
z=h^(64-1)=xxx10100 ^ 111111=x10100 ^ 111111
當x為1時:z=10100,重新雜湊在原位置
當x=0時,z=110100=10100+100000=10100+oldCap,重新雜湊在20+oldCap這個位置,對應程式碼:
newTab[j + oldCap] = hiHead;
這樣上面的程式碼就好理解了,再解釋這幾個變數
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
loHead 、loTail 分別代表舊雜湊桶重新雜湊之後,仍然待在舊的雜湊桶的元素組成的連結串列首尾;相應的hiHead 、hiTail 分別代表舊雜湊桶重新雜湊之後,被對映到新的雜湊桶的元素組成的連結串列首尾.
remove操作
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* Implements Map.remove and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue if true only remove if value is equal
* @param movable if false do not move other nodes while removing
* @return the node, or null if none
*/
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;
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;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//第一個就是需要刪除的節點
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
//遍歷當前雜湊桶所有的節點,退出時總是有e=p.next,node=e,e指向需要刪除的節點
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
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)
//根據上面的關係,說明需要刪除的節點是第一個
tab[index] = node.next;
else
//根據上面的關係,node為需刪除的節點,去掉其引用,由垃圾回收器回收
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
關於HashMap還有一個很重要的操作,就是遍歷,這一部分放到另一篇