引言
本次分析源碼是JDK8版本,我們還是按照之前的分析流程,先來看看HashMap的結構圖
1、HashMap結構圖
HashMap繼承AbstractMap,實現了Map、Cloneable、Serializable接口,Map 是 Key-Value 對映射的抽象接口,HashMap 是基於哈希表的 Map 接口的實現,支持複製、序列化的,如下圖所示:
2、分析源碼
2.1、構造器
//構造方法1
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//構造方法2
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//構造方法3
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);
}
//構造方法4
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
上圖是HashMap的四個構造方法,我們重點來看第三個構造方法,因爲裏面有兩個重要的參數,容量(capacity)和負載因子(load factor),這裏解釋下:
capacity:我們都知道HashMap底層是數組+鏈表的數據結構,這裏的capacity可以簡單的理解爲數組的長度,官方解釋爲buckets(桶)
load factor:load factor就是buckets填滿程度的最大比例,當存放的buckets大於capacityload factor時,數組擴大爲當前的2倍。
threshold:發生擴容的閥值,數值爲capacityload factor
【提示】:默認的capacity爲16,默認的load factor是0.75,默認的threshold爲12
2.2、put方法
put函數大致的思路爲:
- 對key的hashCode()做hash,然後再計算index;
- 如果沒碰撞直接放到bucket裏;
- 如果碰撞了,以鏈表的形式存在buckets後;
- 如果碰撞導致鏈表過長(大於等於TREEIFY_THRESHOLD),就把鏈表轉換成紅黑樹;
- 如果節點已經存在就替換old value(保證key的唯一性)
- 如果bucket滿了(超過load factor*current capacity),就要resize。 具體代碼的實現如下:
public V put(K key, V value) {
// 對key的hashCode()做hash算法
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;
// tab爲空則創建
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 計算index,並對null做處理
if ((p = tab[i = (n - 1) & hash]) == null)
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))))
e = p;
// 該鏈爲樹
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
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;
}
}
// 寫入
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 超過capacity * load factor 則進行擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
2.3、get方法
理解了put方法,再看get方法就很容易了,思路如下:
- 如果bucket裏的第一個節點,直接命中,就返回第一個節點;
- 如果有衝突,則通過key.equals(k)去查找對應的entry
- 若爲樹,則在樹中通過key.equals(k)查找,時間複雜度O(logn);
- 若爲鏈表,則在鏈表中通過key.equals(k)查找,時間複雜度O(n)。 具體代碼的實現如下:
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 && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果直接命中
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 {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
3、總結
3.1、HashMap的特點
HashMap存儲着Entry(hash, key, value, next)對象,可以接收null的鍵值,是非同步的。
3.2、HashMap的工作原理
通過hash的方法,通過put和get存儲和獲取對象。存儲對象時,我們將K/V傳給put方法時,它調用hashCode計算hash從而得到bucket位置,進一步存儲,HashMap會根據當前bucket的佔用情況自動調整容量(超過Load Facotr則resize爲原來的2倍)。獲取對象時,我們將K傳給get,它調用hashCode計算hash從而得到bucket位置,並進一步調用equals()方法確定鍵值對。如果發生碰撞的時候,Hashmap通過鏈表將產生碰撞衝突的元素組織起來,在Java 8中,如果一個bucket中碰撞衝突的元素超過某個限制(默認是8),則使用紅黑樹來替換鏈表,從而提高速度。
3.3、初始化集合,指定的參數initialCapacity大小
initialCapacity = (需要存儲的元素個數 / 負載因子) + 1,負載因子(即loader factor)默認爲0.75,如果無法確定元素個數,則使用默認值爲16。
結束語
HashMap的擴容是非常耗費性能的,所以能判斷元素個數的,最好指定一個初始容量,關於擴容的方法,我們下一篇單獨拿出來講,因爲JDK8在JDK7的基礎上進行了一點改變。
如果本篇對你有所幫助,請順手點個贊,謝謝!