HashMap
public class HashMap<K, V> extends AbstractMap<K, V>
implements Map<K, V>, Cloneable, Serializable {
}
1. 一些重要引數
1.1 serialVersionUID屬性
// 序列化版本號
private static final long serialVersionUID = 362498820763181265L;
serialVersionUID適用於java序列化機制。簡單來說,JAVA序列化的機制是通過 判斷類的serialVersionUID來驗證的版本一致的。在進行反序列化時,JVM會把傳來的位元組流中的serialVersionUID於本地相應實體類的serialVersionUID進行比較。如果相同說明是一致的,可以進行反序列化,否則會出現反序列化版本一致的異常,即是InvalidCastException。
具體序列化的過程是這樣的:序列化操作時會把系統當前類的serialVersionUID寫入到序列化檔案中。當反序列化時系統會自動檢測檔案中的serialVersionUID,判斷它是否與當前類中的serialVersionUID一致。如果一致說明序列化檔案的版本與當前類的版本是一樣的,可以反序列化成功,否則就失敗;
1.2 DEFAULT_INITIAL_CAPACITY屬性
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
DEFAULT_INITIAL_CAPACITY
是HashMap
的預設初始容量,大小為16
1.3 DEFAULT_LOAD_FACTOR屬性
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
DEFAULT_LOAD_FACTOR
是HashMap
的預設負載因子大小,大小為0.75。
當元素數量 ≥ 容量*負載因子,那麼HashMap
需要擴容。
1.4 MAXIMUM_CAPACITY屬性
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
MAXIMUM_CAPACITY
屬性是HashMap
的最大容量。
1.5 TREEIFY_THRESHOLD屬性
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
TREEIFY_THRESHOLD
屬性是HashMap
轉變為紅黑樹實現的閾值。當元素數量大於這個值,就會轉變為紅黑樹。
1.6 UNTREEIFY_THRESHOLD屬性
UNTREEIFY_THRESHOLD
是HashMap
由紅黑樹轉變為連結串列的閾值。當元素數量大於這個值,就會轉變為連結串列。
1.7 MIN_TREEIFY_CAPACITY屬性
MIN_TREEIFY_CAPACITY
是可以將連結串列轉變為紅黑樹的最小陣列容量。如果沒有這個限制,假如節點過多,節點陣列會頻繁地resize
。
2. 一些重要屬性
2.1 table屬性
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K, V>[] table;
table
屬性用來存節點。
2.2 entrySet屬性
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
transient Set<Map.Entry<K, V>> entrySet;
entrySet
屬性用來快取鍵值對集合。
2.3 size屬性
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
size
屬性用來存map的鍵值對數目。
2.4 modCount屬性
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;
modCount
屬性記錄了map結構被修改的次數,用於在迭代器中的快速失敗。
2.5 threshold屬性
/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;
threshold
屬性記錄了下一個需要resize的size大小
2.6 loadFactor屬性
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;
loadFactor
屬性記錄了當前map的負載因子
2.7 Node內部類
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;
}
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) {
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;
}
}
Node內部類實則表達了一個鍵值對, 幷包含了雜湊值和next指標。
2.8 EntrySet內部類
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
public final boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Object value = e.getValue();
return removeNode(hash(key), key, value, true, true) != null;
}
return false;
}
public final Spliterator<Map.Entry<K,V>> spliterator() {
return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (Node<K,V> e : tab) {
for (; e != null; e = e.next)
action.accept(e);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
EntrySet
內部類用來表示當前HashMap
的鍵集合。
3. 一些工具方法
3.1 hash方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
這個hash()
方法的(h = key.hashCode()) ^ (h >>> 16)
過程值得研究。
首先,h >>> 16
是將雜湊值向右無符號移動16位,也就相當於取了原值的高16位。
0000 0100 1011 0011 1101 1111 1110 0001 >>> 16
得到:
0000 0000 0000 0000 0000 0100 1011 0011
然後,我們再來看一個JDK1.7中的indexFor()
方法。在JDK8以後,原始碼中也大量出現tab[(n - 1) & hash]
的形式,實際上也是一樣的。
static int indexFor(int h, int length) {
return h & (length-1);
}
indexFor()
方法的返回值就是table
陣列中我們想要找到的陣列下標。但由於絕大多數情況下length一般都小於2^16即小於65536。所以return h & (length-1)
結果始終是h的低16位與(length-1)進行&運算。
所以很顯然的是,我們每次在計算下標的時候,都幾乎只能用到低位。如果我們想一個辦法,把高位也利用起來,那麼就可以增加雜湊的程度。所以hash()
方法中選擇了與自身的高十六位(h >>> 16)進行異或,來利用到高位。
3.2 tableSizeFor方法
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
tableSizeFor
方法用於計算引數cap對應的resize
閾值。往往出現為下面的語句。
threshold = tableSizeFor(size);
3.3 resize方法
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) {// 如果舊容量不為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;
} else if (oldThr > 0) // 如果舊容量為0,且舊擴容閾值大於0
// 那麼就讓新容量變為舊擴容閾值
newCap = oldThr;
else { // 舊容量和舊擴容閾值都為0
// 令新容量為預設初始容量,並計算擴容閾值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {// 如果新擴容閾值為0
// 計算當前的容量*負載因子
float ft = (float) newCap * loadFactor;
// 設定新擴容閾值為ft或Integer.MAX_VALUE
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
(int) ft : Integer.MAX_VALUE);
}
// 更新擴容閾值
threshold = newThr;
// 建立一個新的table,大小為先前計算出來的新容量
@SuppressWarnings({"rawtypes", "unchecked"})
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
// 更新table指標
table = newTab;
if (oldTab != null) {// 如果舊陣列不為null
for (int j = 0; j < oldCap; ++j) {// 遍歷之
Node<K, V> e;
if ((e = oldTab[j]) != null) {// 如果j處的舊陣列值不為null,存進e
// 將舊陣列裡的值設為null
oldTab[j] = null;
if (e.next == null)// 如果e.next為null
// 在新陣列中找到並賦值
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)// 如果e這裡是一棵樹,呼叫split分割樹,並帶到新陣列中
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
else { // e這裡是連結串列,且e.next不為null
// 下面要進行一個連結串列分割操作,將原連結串列分為lo串和hi串兩串。
Node<K, V> loHead = null, loTail = null;
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
do {
// 儲存next
next = e.next;
/* 通過e.hash & oldCap是0還是1,可以判斷e應該在原串上還是在新串上。
hashMap擴容前後容量大小都是2的冪,假如原容量size二進位制形式是1000,新容量二進位制形式是10000
又由於陣列下標位置的計算公式是e.hash & (size-1),那麼原來是hash & 0111,後來是hash & 01111。
也就實際上是多用了hash的高一位用來確定位置。那麼hash&size就可以知道hash值中高的這一位是0還是1
如果高的一位是0,那麼新位置和原位置一樣,不變。如果高的一位是1,那麼就要移動位置。
*/
if ((e.hash & oldCap) == 0) {// 如果位置不用改變
// 將e加入到lo串中
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} else {// 如果位置需要改變
// 將e加入到hi串中
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);// 如果e賦為next後不為null
// 將lo串放到下標為j的位置處,即不變的位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 將hi串放到下標為j+oldCap處,因為新位置的hash中高一位是1,那下標就要加一個oldCap
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
先嚐試用數學方法確定新容量和新擴容閾值。
隨後遍歷陣列,並深入遍歷其中的連結串列/紅黑樹。如果是連結串列,要將他分裂為lo連結串列和hi連結串列。
3.4 treeifyBin方法
final void treeifyBin(Node<K, V>[] tab, int hash) {
int n, index;
Node<K, V> e;
// 如果陣列很空或很小,那麼就resize()
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {// hash對應的位置在陣列中不為null,就可以進入這個迴圈
TreeNode<K, V> hd = null, tl = null;
do {
// 將節點e轉變為一個樹節點存到變數p
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);
}
}
treeifyBin
方法將符合要求的連結串列轉變為一個紅黑樹。
4. 一些業務方法
4.1 get方法
public V get(Object key) {
Node<K, V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
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 // table不為空
&& (n = tab.length) > 0 // table長度不為0
&& (first = tab[(n - 1) & hash]) != null) {// 在算出來的位置拿到的Node不為null,這是連結串列頭
if (first.hash == hash // 檢查first的雜湊值,假如正確且key也符合
&& ((k = first.key) == key || (key != null && key.equals(k))))
return first;// 直接返回first
if ((e = first.next) != null) {// 如果first連結串列長度不止為1
if (first instanceof TreeNode) {// 如果table當前位置延伸出的是一個紅黑樹
return ((TreeNode<K, V>) first).getTreeNode(hash, key);// 呼叫getTreeNode方法
}
// 不是紅黑樹,是連結串列,遍歷查詢即可
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
詳見上面的註釋。用雜湊值計算出陣列中的位置,再沿著連結串列或紅黑樹去找。
4.2 put方法
public V put(K key, V value) {
// 呼叫putVal
return putVal(hash(key), key, value, false, true);
}
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)
// 如果table為null或者為空,先擴容,再把長度賦值為n
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果找到的指定位置為空,那麼在這裡用新鍵值對呼叫newNode
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也吻合
// 將當前節點p賦給e
e = p;
else if (p instanceof TreeNode)// 如果當前p是一個樹節點,呼叫putTreeVal方法
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else {// 如果當前是一個連結串列節點
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {// 令e=p.next,如果它為null
// 令p.next為用新鍵值對建立的newNode,就是將新節點插在了連結串列尾
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,for迴圈一開始的時候e=p.next,所以相當於p後移了
p = e;
}
}
if (e != null) { // 如果key已經存在,更新它並返回oldValue
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 檢查是否需要擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
詳見上面的註釋。用雜湊值計算出陣列中的位置,再在連結串列或紅黑樹中搜尋,做更新操作或newNode操作。
4.3 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) {
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
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
// 如果是樹,就呼叫對應的方法找到,賦值給node
node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
else {
// 是連結串列,就遍歷查詢
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
// 找到了,將當前節點賦值給node
node = e;
// 退出迴圈,這導致了當找到後,下面的p=e沒有執行
break;
}
// 將當前節點賦值給p
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)
// 如果要移除的是連結串列頭,那麼根據上面do-while塊中的邏輯,node==p,那麼我們直接讓連結串列從node.next開始
tab[index] = node.next;
else
// 如果要移除的不是連結串列頭,那麼根據上面do-while塊中的邏輯node!=p,之前是p->node,刪除node
p.next = node.next;
// 處理modCount和size屬性
++modCount;
--size;
afterNodeRemoval(node);
// 返回remove掉的節點node
return node;
}
}
return null;
}
找->刪,非常清晰的邏輯
4.4 clone方法
@Override
public Object clone() {
HashMap<K,V> result;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
result.reinitialize();
result.putMapEntries(this, false);
return result;
}
clone()
方法對HashMap
做了一個淺拷貝。
5. 一些來自JDK8的方法
下面的方法來自於重寫JDK8中的Map介面中的方法。
一些函式式的方法在此不做解析,如compute()
和merge()
等。
5.1 getOrDefault / putIfAbsent
@Override
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}
很好理解,獲得實際值或預設值/不存在時插入。
5.2 remove / replace
@Override
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
當對應鍵值對存在時,進行刪除\替換操作。