HashMap原始碼解析(基於JDK1.8)
雜湊是一種用於以常數平均時間執行插入、刪除和查詢的技術。HashMap是基於雜湊表的Map介面實現,該實現提供了所有可選的對映操作,並允許使用 空值和空鍵。(HashMap 類大致等同於Hashtable,除了它是不同步的並且允許為空值。)這個類不能保證Map的順序; 特別是,它不能保證順序會隨著時間的推移保持不變。
下面就開始我們的原始碼分析吧:
老規矩,先貼一張UML圖:
①:HashMap直接繼承自AbstractMap抽象類,在AbstractMap已經實現了Map介面中的所有方法,只有public abstract Set<Entry<K,V>> entrySet()
②HashMap實現了Map介面,Map中不能使用Map作為key,但Map可以作為value,但是不建議這樣做。下圖是JDK1.8官方文件給出的解釋。
③實現了Cloneable表明HashMap支援可克隆。
④實現了Serializable介面,表明HashMap支援序列化。
好啦,終於到了原始碼分析的部分了:
下面是HashMap儲存結構圖:
(該圖來自美團技術團隊,我分析HashMap的時候,也參考了這裡的內容。)
老規矩,我們還是先分析HashMap中的例項域部分(也就是HashMap的屬性),但是HashMap比較複雜,它的例項域部分的型別是HashMap中的一個內部類,而對應的內部類又實現了Map介面中宣告的一個介面(Map.Entry<K,V>
Map.Entry<K,V>
)開始看起吧。其實很簡單的….
1:我們先看看HashMap中的雜湊桶(Node<K,V>
),它是HashMap中的一個靜態內部類,它實現了Map.Entry<K,V>
介面,該介面是Map介面內宣告的一個內部介面,它表示Map中的一個實體(也就是一個鍵值對(key-value對))
2:我們先看看Map.EnTRY<K,V>
這個介面的原始碼:
從程式碼中可以看出,我們可以直接通過該Entry物件獲取相應的鍵和值。它提供了很多有用的方法,程式碼很簡單。不過最後提供了JDK1.8之後新增的方法,主要基於Stream做操作,目前我也僅僅停留在會用階段,原始碼分析不來(⊙o⊙)…(後面關於Stream的部分的分析直接跳過(艹皿艹 ))
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}
public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getValue().compareTo(c2.getValue());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
}
3:我們先看一下HashMap中的第一個欄位:transient Node<K,V>[] table
,它就是我們的哈系桶陣列。接下來給出Node<K,V>
的原始碼。
4:Node<K,V>
(哈系桶),它實現了Map.Entry<K,V>
介面,下面給出原始碼:
下面我們逐個介紹一下它的欄位的作用:
hash:主要用來定位當前Node在陣列位置的索引。
key:鍵值對中的鍵。
value:鍵值對中的值。
Node<K,V>
next指代連結串列中的下一個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;
}
//該方法實現Map.Entry<K,V>介面。
public final K getKey() { return key; }
//該方法實現Map.Entry<K,V>介面。
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//該方法實現Map.Entry<K,V>介面。設定新值,並返回舊值。
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;
}
}
5:接著我們看一下HashMap的其他欄位:
// HashMap對應的序列化ID。
private static final long serialVersionUID = 362498820763181265L;
//HashMap的雜湊桶陣列預設的初始化容量(2的4次方(也就是16))
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//HashMap的雜湊桶陣列的最大容量(2的30次方)
static final int MAXIMUM_CAPACITY = 1 << 30;
//預設的載入因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//HashMap中一個桶的樹化的閥值(一旦在同一個雜湊桶中的元素超過8個,
//連結串列就會變成紅黑樹)
//(這個也是JDK 1.8的新特性)
static final int TREEIFY_THRESHOLD = 8;
/HashMap中一個桶的連結串列化的閥值(一旦同一個雜湊桶中元素少於6個,紅黑樹就變成了連結串列)
static final int UNTREEIFY_THRESHOLD = 6;
//哈系桶陣列的最小樹形化容量
//當雜湊桶的容量大於這個值時,表中的桶才能進行樹形化
static final int MIN_TREEIFY_CAPACITY = 64;
//HashMap中鍵值對的個數
transient int size;
//HashMap被修改的次數(主要用於快速失敗(ArrayList和LinkedList等執行緒不安全的類都用到了這個變數))
transient int modCount;
//HashMap中實際允許儲存的鍵值對的最大數量
//(一旦超過這個值,表明哈系衝突很嚴重了,就需要擴容了。)
//threshold = table.length*loadFactor(也就是雜湊桶陣列的長度*載入因子)
int threshold;
//載入因子(預設值是0.75)
final float loadFactor;
//HashMap中的鍵值對快取在entrySet中,即使key在外部修改導致hashCode變化,該快取中
//仍然可以找到對映關係。
transient Set<Map.Entry<K,V>> entrySet;
6:接下來是HashMap的構造函數了:
①預設建構函式:
//預設的建構函式(只例項化了裝載因子為0.75)
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
②需要提供兩個引數,一個是初始化容量,一個是裝載因子。
其中的tableSizeFor(initialCapacity)
主要用於找到大於等於initialCapacity的最小的2的冪。
那為麼要這麼做呢?
首先雜湊桶陣列的大小必須為2的n次冪,至於為什麼必須是2的n次冪,這裡暫且不解釋,放到在為HashMap第一次新增元素的時候再說,而且此時我們呼叫該構造方法初始化之後,它並沒有初始化雜湊桶陣列(Node<K,V>[] table
),而且此時threshold的值也並不是table.length * Node<K,V>[] table
,而是一個大於或等於initialCapacity的最小的2的冪。而且哈係數組也是在我們第一次新增元素的時候初始化的。*
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);
}
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;
}
③只提供一個初始化容量,呼叫上面的那個構造器,裝載因子設定為預設的裝載因子。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
⑤建構函式中需要一個Map作為引數:
其中裝載因子使用的預設裝載因子0.75。接著呼叫putMapEntries()方法。
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
當是初始化構造map的時候,evict為false,如果已經初始化完成後,evict為true
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//把map實際儲存鍵值對的大小用一個變數s儲存起來。
int s = m.size();
//如果s大於零(鍵值對的個數大於0)
if (s > 0) {
//如果HashMap中哈系桶陣列為空
if (table == null) { // pre-size
//根據m的鍵值對的數量和HashMap的裝載因子計算閥值。
float ft = ((float)s / loadFactor) + 1.0F;
//限制閥值不能超過MAXIMUM_CAPACITY
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//如果新閥值t大於HashMap的當前閥值時,則需要重新計算閥值,
//(呼叫上面我們已經分析過的方法)最後賦值給當前HashMap的閥值。
if (t > threshold)
threshold = tableSizeFor(t);
}
//接著判斷如果m的鍵值對大小大於當前HashMap的threshold,則需要
//呼叫resize()方法進行擴容。
else if (s > threshold)
resize();
//最後通過遍歷m中的鍵值對,把鍵值對新增到我們當前的HashMap中。
//putVal()方法的具體分析我們後面講。
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);
}
}
}
7:接下來就要看我們的put()方法(HashMap很多知識點都在這個方法中)
從下面的方法中,我們可以看出,首先需要計算key(鍵)的雜湊值,然後通過雜湊值定位當前鍵值對在我們的哈系桶陣列的索引。那我們先看一下這個hash()方法。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
8:下面是我們計算鍵的哈系值的方法
先計算鍵的雜湊值,如果鍵為空的話,則返回雜湊值為0,如果不空的話,就把該鍵hashCode值的高16位與鍵的低16位做異或運算,把計算後的值返回作為鍵的哈系值。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
9:下面是putVal()方法
程式碼分析如下圖所示:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//首先分別定義了一個指向雜湊桶陣列引用tab,和一個雜湊桶引用p,
//n代表雜湊桶陣列的長度
//i指代雜湊桶陣列的下標。
Node<K,V>[] tab; Node<K,V> p; int n, i;
//接著判斷雜湊桶陣列是否為空,或者判斷雜湊桶陣列的長度是否為0
if ((tab = table) == null || (n = tab.length) == 0)
//如果條件成立,則需要擴容。(當使用的不是傳入map引數的構造器時,第一次新增元素的
//時候,才進行哈係數組的初始化。後面會講到resize()方法。)
//最後把擴容後的雜湊桶陣列的長度賦值給n。
n = (tab = resize()).length;
//接著通過key的雜湊值和雜湊桶陣列的長度減一做與運算,就可以得到該鍵值對
//對應的雜湊桶陣列的下標了。其實這裡也是為什麼會把雜湊桶陣列的大小設定為2的n次方的
//原因,當雜湊桶陣列的長度總是2的n次方的時候,(n-1)&hash等價於hash對雜湊桶陣列
//的長度取模,也就是hash%table.length,但是&比%具有更高的效率。至於為什麼等價,
//你可以看看[下面這個連結中的部落格,看完你就明白了。](https://blog.csdn.net/aliway89/article/details/43152513),
//計算好雜湊桶陣列的索引後,判斷一下該索引位置上的元素是否為空,如果
//為空的話,則直接把鍵值對新增到該位置上。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//如果計算後的陣列的下標對應的位置的元素不為空的話,定義一個新的Node<K,V>,和K。
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;
//接著判斷p是不是紅黑樹節點。(當同一個雜湊桶中元素超過8個,就會
//變成紅黑樹節點。)
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果不是紅黑樹節點,則依次遍歷連結串列中的節點,直到找到連結串列中某個節點
//的下一個節點為空,則把對應的鍵值對填充進去,接著判斷連結串列的節點數
//是否超過8,如果超過8的話,連結串列就會轉化為紅黑樹。
//最後跳出for迴圈。
//或者是當前鍵和雜湊桶中的鍵的哈系值相等並且鍵的值也相等,則跳出迴圈。
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;
}
}
//如果key已經存在了,則把鍵對應的值替換掉,返回舊值。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//允許LinkedHashMap後動作的回撥
afterNodeAccess(e);
return oldValue;
}
}
//最後把modCount++(用於快速失敗)
++modCount;
//如果HashMap中鍵值對的個數大於HashMap允許的最大容量時,則進行擴容。
if (++size > threshold)
resize();
//允許LinkedHashMap後動作的回撥
afterNodeInsertion(evict);
return null;
}
10:擴容方法:
final Node<K,V>[] resize() {
//首先把舊的雜湊桶陣列用一個引用儲存起來。
Node<K,V>[] oldTab = table;
//把舊雜湊桶陣列的長度也儲存起來。
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//把舊雜湊桶陣列允許的最大鍵值對數也儲存起來。
int oldThr = threshold;
//定義新的長度和容量。
int newCap, newThr = 0;
//在舊的雜湊桶陣列的長度大於0的情況下進行判斷。
if (oldCap > 0) {
//如果就的雜湊桶陣列的長度大於等於所允許的最大雜湊桶陣列的長度時
if (oldCap >= MAXIMUM_CAPACITY) {
//則把最大雜湊桶陣列的長度給HashMap中所允許的最大鍵值對的個數。
threshold = Integer.MAX_VALUE;
return oldTab;
}
//否則判斷新的雜湊桶陣列的長度是舊的雜湊桶陣列長度的兩倍且必須
//小於最大容量。而且得滿足舊的容量大於預設的初始化容量。
//把允許的最大鍵值對數擴容為原來的兩倍。
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 如果舊的允許的最大鍵值對數大於0,則新的容量等於舊的允許的最大鍵值對數。
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];
table = newTab;
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;
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;
}
總結:
①對於HashMap底層實現的學習,建議還是通過debug的方式,這樣通過IDE輔助的方式,能夠更好地理解HashMap的底層實現。
②下面我們給出一個例子,當同一個雜湊桶中的元素超過8個的時候,連結串列就會變成紅黑樹。
因為整型的數字的雜湊值等於本身,下面依次新增的元素計算的陣列索引都是1,索引當同一個雜湊桶中元素超過8後,相應的連結串列就會變成紅黑樹,你可以打個斷點debug一下。
HashMap<Integer,Integer> hashMap = new HashMap<>();
hashMap.put(1,1);
hashMap.put(17,2);
hashMap.put(33,3);
hashMap.put(49,4);
hashMap.put(65,5);
hashMap.put(81,6);
hashMap.put(113,7);
hashMap.put(129,8);
// 當衝突超過8後,會變成紅黑樹
hashMap.put(145,9);