1. 前言
Hashmap可以說是Java面試必問的,一般的面試題會問:
- Hashmap有哪些特性?
- Hashmap底層實現原理(get\put\resize)
- Hashmap怎么解決hash沖突?
- Hashmap是線程安全的嗎?
- ...
今天就從源碼角度一探究竟。筆者的源碼是OpenJDK1.7
2. 構造方法
首先看構造方法的源碼
// 默認初始容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 默認負載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 數組, 該數據不參與序列化
transient Entry[] table;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 初始容量16,擴容因子0.75,擴容臨界值12
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
// 基礎結構為Entry數組
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
由以上源碼可知,Hashmap的初始容量默認是16, 底層存儲結構是數組(到這里只能看出是數組, 其實還有鏈表,下邊看源碼解釋)。基本存儲單元是Entry,那Entry是什么呢?我們接着看Entry相關源碼,
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next; // 鏈表后置節點
final int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n; // 頭插法: newEntry.next=e
key = k;
hash = h;
}
...
}
由Entry源碼可知,Entry是鏈表結構。綜上所述,可以得出:
Hashmap底層是基於數組和鏈表實現的
3. Hashmap中put()過程
我已經將put過程繪制了流程圖幫助大家理解

先上put源碼
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
// 根據key計算hash
int hash = hash(key.hashCode());
// 計算元素在數組中的位置
int i = indexFor(hash, table.length);
// 遍歷鏈表,如果相同覆蓋
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 頭插法插入元素
addEntry(hash, key, value, i);
return null;
}
上圖中多次提到頭插法,啥是 頭插法 呢?接下來看 addEntry 方法
void addEntry(int hash, K key, V value, int bucketIndex) {
// 取出原bucket鏈表
Entry<K,V> e = table[bucketIndex];
// 頭插法
table[bucketIndex] = new Entry<>(hash, key, value, e);
// 判斷是否需要擴容
if (size++ >= threshold)
// 擴容好容量為原來的2倍
resize(2 * table.length);
}
結合Entry類的構造方法,每次插入新元素的時候,將bucket原鏈表取出,新元素的next指向原鏈表,這就是 頭插法 。為了更加清晰的表示Hashmap存儲結構,再繪制一張存儲結構圖。

4. Hashmap中get()過程
get()邏輯相對比較簡單,如圖所示

我們來對應下get()源碼
public V get(Object key) {
// 獲取key為null的值
if (key == null)
return getForNullKey();
// 根據key獲取hash
int hash = hash(key.hashCode());
// 遍歷鏈表,直到找到元素
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
5. Hashmap中resize()過程
只要是新插入元素,即執行addEntry()方法,在插入完成后,都會判斷是否需要擴容。從addEntry()方法可知,擴容后的容量為原來的2倍。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 新建數組
Entry[] newTable = new Entry[newCapacity];
// 數據遷移
transfer(newTable);
// table指向新的數組
table = newTable;
// 新的擴容臨界值
threshold = (int)(newCapacity * loadFactor);
}
這里有個transfer()方法沒講,別着急,擴容時線程安全的問題出現在這個方法中,接下來講解數組復制過程。
6. Hashmap擴容安全問題
大家都知道結果: 多線程擴容有可能會形成環形鏈表,這里用圖給大家模擬下擴容過程。
首先看下單線程擴容的頭插法

然后看下多線程可能會出現的問題

以下是源碼,你仔細品一品
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
// 釋放舊Entry數組的對象引用
src[j] = null;
do {
Entry<K,V> next = e.next;
// 重新根據新的數組長度計算位置(同一個bucket上元素hash相等,所以擴容后必然還在一個鏈表上)
int i = indexFor(e.hash, newCapacity);
// 頭插法(同一位置上新元素總會被放在鏈表的頭部位置),將newTable[i]的引用賦給了e.next
e.next = newTable[i];
// 將元素放在數組上
newTable[i] = e;
// 訪問下一個元素
e = next;
} while (e != null);
}
}
}
7. Hashmap尋找bucket位置
static int indexFor(int h, int length) {
// 根據hash與數組長度mod運算
return h & (length-1);
}
由源碼可知, jdk根據key的hash值和數組長度做mod運算,這里用位運算代替mod。
hash運算值是一個int整形值,在java中int占4個字節,32位,下邊通過圖示來說明位運算。

8. AD
如果您覺得還行,請關注公眾號【當我遇上你】, 您的支持是我輸出的最大動力。
同時,歡迎大家一起交流學習。

