手寫Java HashMap核心源碼
上一章手寫LinkedList核心源碼,本章我們來手寫Java HashMap的核心源碼。
我們來先了解一下HashMap的原理。HashMap 字面意思 hash + map,map是映射的意思,HashMap就是用hash進行映射的意思。不明白?沒關系。我們來具體講解一下HashMap的原理。
HashMap 使用分析
//1 存
HashMap<String,String> map = new HashMap<>();
map.put("name","tom");
//2 取
System.out.println(map.get("name"));//輸出 tom
使用就是這么簡單。
HashMap 原理分析
我們知道,Object類有一個hashCode()方法,返回對象的hashCode值,可以理解為返回了對象的內存地址,暫且不管返回的是內存地址或者其它什么也好,先不管,至於hashCode()方法回返的數是怎么算的?我們也不管
第1 我們只需要記住:這個函數返回的是一個數就行了。
第2 HashMap內部是用了一個數組來存放數據
1 HashMap是如何把 name,tom存放的?
下面我們用一張圖來演示

從上圖可以看出:
注:上圖中數組的大小是7,是多少都行,只是我們這里就畫了7個元素,我們就以數組大小為7來說明HashMap的原理。
- 數組的大小是7,那么數組的索引范圍是[0 , 6]
- 取得key也就是"name"的hashCode,這是一個數,不管這個數是多少,對7進行取余數,那么范圍肯定是 [0 , 6],正好和數組的索引是一樣的。
- "name".hashCode() % 7 的值假如為2 ,那么value也就是"tom"應該存放的位置就是2
- data[2] = "tom" ,存到數組中。是不是很巧妙。
2 下面再來看看如何取?
也用一張圖來演示底層原理,如下

由上圖可知:
- 首先也是獲取key也就是"name"的hashCode值
- 用hashCode值對數組的大小 7 進行取余數,和存的時候運行一樣,肯定也是2
- 從數組的第 2 個位置把value取出,即: String value = data[2]
注:有幾點需要注意
- 某個對象的hashCode()方法返回的值,在任何時候調用,返回的值都是一樣的
- 對一個數 n 取余數 ,范圍是 [ 0, n - 1 ]
注:有幾個問題需要解決
-
存的時候,如果不同的key的hashCode對數組取余數,都正好相同了,也就是都映射在了數組的同一位置,怎么辦?這就是hash沖突問題
比如9 % 7 == 2 , 16 % 7 == 2都等於2
答:數組中存放的是一個節點的數據結構,節點有next屬性,如果hash沖突了,單鏈表進行存放,取的時候也是一樣,遍歷鏈表 -
如果數組已經存滿了怎么辦?
答:和ArrayList一樣,進行擴容,重新映射 -
直接使用hashCode()值進行映射,產生hash沖突的概論很大,怎么辦?
答:參考JDK中HashMap中的實現,有一個hash()函數,再對hashCode()的值進行運行一下,再進行映射
由上可知:HashMap是用一個數組來存放數據,如果遇到映射的位置上面已經有值了,那么就用鏈表存放在當前的前面。數組+鏈表結構,是HashMap的底層結構
假如我們的數組里面存放的元素是QEntry,如下圖:

手寫 HashMap 核心源碼
上面分析了原理,接下來我們用最少的代碼來提示HashMap的原理。
我們就叫QHashMap類,同時數組里面的元素需要也需要定義一個類,我們定義在QHashMap類的內部。就叫QEntry
QEntry的定義如下:
//底層數組中存放的元素類
public static class QEntry<K, V> {
K key; //存放key
V value; //存放value
int hash; //key對應的hash值
//hash沖突時,也就是映射的位置上已經有一個元素了
//那么新加的元素作為鏈表頭,已經存放的放在后面
//即保存在next中,一句話:添加新元素時,添加在表頭
QEntry<K, V> next;
public QEntry(K key, V value, int hash, QEntry<K, V> next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
}
QEntry類的定義有了,下面看下QHashMap類中需要哪些屬性?
QHashMap類的定義如下圖:
public class QHashMap<K, V> {
//默認的數組的大小
private static final int DEFAULT_INITIAL_CAPACITY = 16;
//默認的擴容因子,當數據中元素的個數越多時,hash沖突也容易發生
//所以,需要在數組還沒有用完的情況下就開始擴容
//這個 0.75 就是元素的個數達到了數組大小的75%的時候就開始擴容
//比如數組的大小是100,當里面的元素增加到75的時候,就開始擴容
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
//存放元素的數組
private QEntry[] table;
//數組中元素的個數
private int size;
......
}
只需要兩個常量和兩個變量就夠了。
下面我們看下QHashMap的構造函數,為了簡單,只實現一個默認的構造函數
public QHashMap() {
//創建一個數組,默認大小為16
table = new QEntry[DEFAULT_INITIAL_CAPACITY];
//此時元素個數是0
size = 0;
}
我們來看下QHashMap是如何存放數據的 map.put("name","tom")
put()函數的實現如下:
/**
* 1 參數key,value很容易理解
* 2 返回V,我們知道,HashMap有一個特點,
* 如果調用了多次 map.put("name","tom"); map.put("name","lilei");
* 后面的值會把前面的覆蓋,如果出現這種情況,返回舊值,在這里返回"tom"
*/
public V put(K key, V value) {
//1 為了簡單,key不支持null
if (key == null) {
throw new RuntimeException("key is null");
}
//不直接用key.hashCode(),我們對key.hashCode()再作一次運算作為hash值
//這個hash()的方法我是直接從HashMap源碼拷貝過來的。可以不用關心hash()算法本身
//只需要知道hash()輸入一個數,返回一個數就行了。
int hash = hash(key.hashCode());
//用key的hash值和數組的大小,作一次映射,得到應該存放的位置
int index = indexFor(hash, table.length);
//看看數組中,有沒有已存在的元素的key和參數中的key是相等的
//相等則把老的值替換成新的,然后返回舊值
QEntry<K, V> e = table[index];
while (e != null) {
//先比較hash是否相等,再比較對象是否相等,或者比較equals方法
//如果相等了,說明有一樣的key,這時要更新舊值為新的value,同時返回舊的值
if (e.hash == hash && (key == e.key || key.equals(e.key))) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
e = e.next;
}
//如果數組中沒有元素的key與傳的key相等的話
//把當前位置的元素保存下來
QEntry<K, V> next = table[index];
//next有可能為null,也有可能不為null,不管是否為null
//next都要作為新元素的下一個節點(next傳給了QEntry的構造函數)
//然后新的元素保存在了index這個位置
table[index] = new QEntry<>(key, value, hash, next);
//如果需要擴容,元素的個數大於 table.length * 0.75 (別問為什么是0.75,經驗)
if (size++ >= (table.length * DEFAULT_LOAD_FACTOR)) {
resize();
}
return null;
}
注釋很詳細,這里有幾個函數
hash()函數是直接從HashMap源碼中拷貝的,不用糾結這個算法。
indexFor(),傳入hash和數組的大小,從而知道我們應該去哪個位置查找或保存
這兩個函數的源碼如下:
//對hashCode進行運算,JDK中HashMap的實現,直接拷貝過來了
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
//根據 h 求key落在數組的哪個位置
static int indexFor(int h, int length) {
//或者 return h & (length-1) 性能更好
//這里我們用最容易理解的方式,對length取余數,范圍就是[0,length - 1]
//正好是table數組的所有的索引的范圍
h = h > 0 ? h : -h; //防止負數
return h % length;
}
還有一個擴容函數。當元素的個數大於 table.length * 0.75時,我們就開始擴容
resize()的源碼如下 :
//擴容,元素的個數大於 table.length * 0.75
//數組擴容到原來大小的2倍
private void resize() {
//新建一個數組,大小為原來數組大小的2倍
int newCapacity = table.length * 2;
QEntry[] newTable = new QEntry[newCapacity];
QEntry[] src = table;
//遍歷舊數組,重新映射到新的數組中
for (int j = 0; j < src.length; j++) {
//獲取舊數組元素
QEntry<K, V> e = src[j];
//釋放舊數組
src[j] = null;
//因為e是一個鏈表,有可能有多個節點,循環遍歷進行映射
while (e != null) {
//把e的下一個節點保存下來
QEntry<K, V> next = e.next;
//e這個當前節點進行在新的數組中映射
int i = indexFor(e.hash, newCapacity);
//newTable[i] 位置上有可能是null,也有可能不為null
//不管是否為null,都作為e這個節點的下一個節點
e.next = newTable[i];
//把e保存在新數組的 i 的位置
newTable[i] = e;
//繼續e的下一個節點的同樣的處理
e = next;
}
}
//所有的節點都映射到了新數組上,別忘了把新數組的賦值給table
table = newTable;
}
相比put()函數來說,get()就簡單多了。
只需要通過hash值找到相應的數組的位置,再遍歷鏈表,找到一個元素里面的key與傳的key相等就行了。
put()方法的源碼如下:
//根據key獲取value
public V get(K key) {
//同樣為了簡單,key不支持null
if (key == null) {
throw new RuntimeException("key is null");
}
//對key進行求hash值
int hash = hash(key.hashCode());
//用hash值進行映射,得到應該去數組的哪個位置上取數據
int index = indexFor(hash, table.length);
//把index位置的元素保存下來進行遍歷
//因為e是一個鏈表,我們要對鏈表進行遍歷
//找到和key相等的那個QEntry,並返回value
QEntry<K, V> e = table[index];
while (e != null) {
//比較 hash值是否相等
if (hash == e.hash && (key == e.key || key.equals(e.key))) {
return e.value;
}
//如果不相等,繼續找下一個
e = e.next;
}
return null;
}
上面就是QHashMap的核心源碼,我們沒有實現刪除。
下面是把QHashMap整個類的源碼發出來
QHashMap完整源碼如下:
public class QHashMap<K, V> {
//默認的數組的大小
private static final int DEFAULT_INITIAL_CAPACITY = 16;
//默認的擴容因子,當數組的大小大於或者等於當前容量 * 0.75的時候,就開始擴容
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
//底層用一個數組來存放數據
private QEntry[] table;
//數組大小
private int size;
//一個點節,數組中存放的單位
public static class QEntry<K, V> {
K key;
V value;
int hash;
QEntry<K, V> next;
public QEntry(K key, V value, int hash, QEntry<K, V> next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
}
public QHashMap() {
table = new QEntry[DEFAULT_INITIAL_CAPACITY];
size = 0;
}
//根據key獲取value
public V get(K key) {
//同樣為了簡單,key不支持null
if (key == null) {
throw new RuntimeException("key is null");
}
//對key進行求hash值
int hash = hash(key.hashCode());
//用hash值進行映射,得到應該去數組的哪個位置上取數據
int index = indexFor(hash, table.length);
//把index位置的元素保存下來進行遍歷
//因為e是一個鏈表,我們要對鏈表進行遍歷
//找到和key相等的那個QEntry,並返回value
QEntry<K, V> e = table[index];
while (e != null) {
//比較 hash值是否相等
if (hash == e.hash && (key == e.key || key.equals(e.key))) {
return e.value;
}
//如果不相等,繼續找下一個
e = e.next;
}
return null;
}
/**
* 1 參數key,value很容易理解
* 2 返回V,我們知道,HashMap有一個特點,
* 如果調用了多次 map.put("name","tom"); map.put("name","lilei");
* 后面的值會把前面的覆蓋,如果出現這種情況,返回舊值,在這里返回"tom"
*/
public V put(K key, V value) {
//1 為了簡單,key不支持null
if (key == null) {
throw new RuntimeException("key is null");
}
//不直接用key.hashCode(),我們對key.hashCode()再作一次運算作為hash值
//這個hash()的方法我是直接從HashMap源碼拷貝過來的。可以不用關心hash()算法本身
//只需要知道hash()輸入一個數,返回一個數就行了。
int hash = hash(key.hashCode());
//用key的hash值和數組的大小,作一次映射,得到應該存放的位置
int index = indexFor(hash, table.length);
//看看數組中,有沒有已存在的元素的key和參數中的key是相等的
//相等則把老的值替換成新的,然后返回舊值
QEntry<K, V> e = table[index];
while (e != null) {
//先比較hash是否相等,再比較對象是否相等,或者比較equals方法
//如果相等了,說明有一樣的key,這時要更新舊值為新的value,同時返回舊的值
if (e.hash == hash && (key == e.key || key.equals(e.key))) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
e = e.next;
}
//如果數組中沒有元素的key與傳的key相等的話
//把當前位置的元素保存下來
QEntry<K, V> next = table[index];
//next有可能為null,也有可能不為null,不管是否為null
//next都要作為新元素的下一個節點(next傳給了QEntry的構造函數)
//然后新的元素保存在了index這個位置
table[index] = new QEntry<>(key, value, hash, next);
//如果需要擴容,元素的個數大於 table.length * 0.75 (別問為什么是0.75,經驗)
if (size++ >= (table.length * DEFAULT_LOAD_FACTOR)) {
resize();
}
return null;
}
//擴容,元素的個數大於 table.length * 0.75
//數組擴容到原來大小的2倍
private void resize() {
//新建一個數組,大小為原來數組大小的2倍
int newCapacity = table.length * 2;
QEntry[] newTable = new QEntry[newCapacity];
QEntry[] src = table;
//遍歷舊數組,重新映射到新的數組中
for (int j = 0; j < src.length; j++) {
//獲取舊數組元素
QEntry<K, V> e = src[j];
//釋放舊數組
src[j] = null;
//因為e是一個鏈表,有可能有多個節點,循環遍歷進行映射
while (e != null) {
//把e的下一個節點保存下來
QEntry<K, V> next = e.next;
//e這個當前節點進行在新的數組中映射
int i = indexFor(e.hash, newCapacity);
//newTable[i] 位置上有可能是null,也有可能不為null
//不管是否為null,都作為e這個節點的下一個節點
e.next = newTable[i];
//把e保存在新數組的 i 的位置
newTable[i] = e;
//繼續e的下一個節點的同樣的處理
e = next;
}
}
//所有的節點都映射到了新數組上,別忘了把新數組的賦值給table
table = newTable;
}
//對hashCode進行運算,JDK中HashMap的實現,直接拷貝過來了
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
//根據 h 求key落在數組的哪個位置
static int indexFor(int h, int length) {
//或者 return h & (length-1) 性能更好
//這里我們用最容易理解的方式,對length取余數,范圍就是[0,length - 1]
//正好是table數組的所有的索引的范圍
h = h > 0 ? h : -h; //防止負數
return h % length;
}
}
上面就是QHashMap的原理。下面我們寫一段測試代碼來看下我們的QHashMap能不能正常運行。測試代碼如下:
public static void main(String[] args) {
QHashMap<String, String> map = new QHashMap<>();
map.put("name", "tom");
map.put("age", "23");
map.put("address", "beijing");
String oldValue = map.put("address", "shanghai"); //key一樣,返回舊值,保存新值
System.out.println(map.get("name"));
System.out.println(map.get("age"));
System.out.println("舊值=" + oldValue);
System.out.println("新值=" + map.get("address"));
}
輸出如下:
tom
23
舊值=beijing
新值=shanghai
通過上面的簡單的實現了QHashMap,還有好多功能沒有實現,比較remove,clear,containsKey()等,還有遍歷相關,有興趣的讀者可以自己實現
