1. 實現原理
JDK1.7中的HashMap是由數組+鏈表組成的,而JDK1.8中的HashMap是由數組+鏈表+紅黑樹組成。數組的默認長度(DEFAULT_INITIAL_CAPACITY
)爲16,加載因子(DEFAULT_LOAD_FACTOR
)爲0.75。
- HashMap的默認長度爲16和規定數組長度爲2的冪,是爲了降低哈希碰撞的機率。
- HashMap中使用鏈表主要爲了解決哈希衝突,鏈表出現越少或者長度越小,性能纔會越好。
數組:具有遍歷快,增刪慢的特點。數組在堆中是一塊連續的存儲空間,遍歷速度快,時間複雜度爲
O(1)
;當在中間插入或刪除元素時,會造成該元素後面所有元素地址的改變,所以增刪慢,增刪的時間複雜度爲O(n)
。
鏈表:鏈表具有增刪快,遍歷慢的特點。鏈表中各元素的內存空間是不連續的,一個節點至少包含節點數據與後繼節點的引用,所以在插入刪除時,只需修改該位置的前驅節點與後繼節點即可,增刪時間複雜度爲
O(1)
。但是在遍歷時,get(n)元素時,需要從第一個開始,依次拿到後面元素的地址進行遍歷,直到遍歷到第n個元素,遍歷時間複雜度爲O(n)
,所以遍歷效率極低。
2. 哈希衝突
指兩個元素通過hash函數計算出的值是一樣的,表示這兩個元素存儲的是同一個地址。當後面的元素要插入到這個地址時,發現已經被佔用了,這時候就產生了哈希衝突。
哈希衝突的解決辦法:
- 開放定址法:當發生哈希衝突時,查詢產生衝突的地址的下一個地址是否被佔用,直到尋找到空的地址。
- 鏈地址法:當發生哈希衝突時,在衝突的地址上生成一個鏈表,將衝突的元素的key通過equals進行比較,相同即覆蓋,不同則添加到鏈表上。
HashMap
使用的是鏈地址法。在JDK1.7中,如果鏈表過長,效率就會大大降低,查找和添加操作的時間複雜度都爲O(n)
;在JDK1.8中,如果鏈表長度大於8,鏈表就會轉化爲紅黑樹,時間複雜度也就將爲O(logn)
,性能得到了很大的提升。
當紅黑樹節點個數少於6的時候,又會將紅黑樹轉化爲鏈表。因爲在數據量較小的情況下,紅黑樹要維持平衡,比起鏈表,性能上的優勢並不明顯。
3. Rehash擴容機制
如果HashMap的大小超過了負載因子(默認爲0.75)定義的容量,也就是說,當一個Map填滿了75%的Bucket時候,將會創建原來HashMap大小的兩倍的Bucket數組,來重新調整Map的大小,並將原來的對象放入新的Bucket數組中。
閾值 = 數組默認的長度 x 負載因子(閾值 = 16 x 0.75 = 12)
1. HashMap擴容限制的負載因子爲什麼是0.75?爲什麼不能是0.1或者1呢?
- 如果負載因子爲0.5甚至更低的可能的話,最後得到的臨時閾值明顯會很小,這樣的情況就會造成內存的浪費,存在多餘的沒用的內存空間,也不滿足哈希表均勻分佈的情況。
- 如果負載因子達到了1的情況,也就是數組存滿了才發生擴容,這樣會出現大量的哈希衝突的情況,出現鏈表過長,因此造成get查詢數據的效率。
2. 爲何數組容量必須是2次冪?
索引計算公式爲index = (length - 1) & hash
,如果length爲2次冪,那麼length-1的低位就全是1,哈希值進行與操作時可以保證低位的值不變,效果等同於hash%length
,從而保證分佈均勻。
JDK1.8中在擴容HashMap的時候,不需要像JDK1.7中去重新計算元素的hash,只需要看看原來的hash值新增的哪個二進制數是1還是0就好了「是0還是1可以認爲是隨機的」,如果是0的話表示索引沒有變,是1的話表示索引變成“oldCap+原索引”,這樣即省去了重新計算hash值的時間,並且擴容後鏈表元素位置不會倒置。
4. JDK1.7源碼分析
JDK1.7中的HashMap是由數組+鏈表組成的,組成鏈表結點的是Entity
包含三個元素:key
、value
和指向下一個Entity的next
。
- 如果定位到的數組位置不含鏈表(當前Entity的next指向null),那麼對於查找和添加等操作的執行速度很快,僅需要一次尋址,時間複雜度爲O(1)。
- 如果定位到的數組包含鏈表,對於添加操作,其時間複雜度爲O(n)。首先遍歷鏈表,存在即覆蓋,否則新增;對於查找操作仍需要遍歷鏈表,然後通過
equals
方法逐一比對查找。
4.1 put()
- 用
table[index]
表示通過hash值計算出元素需要存儲在數組中的位置(bucket桶),先判斷該位置上是否存在Entity。 - 如果不存在Entity,在該位置上插入一個
Entity<k,v>
對象,插入結束。 - 如果存在Entity,通過equals方法將key和已有的key進行比較,檢查是否相同。
- 如果相同,新的value替換老的value;
- 如果不相同,則在table[index]插入該Entity,並將新的Entity的next指向原來的Entity,新插入的Entity的位置永遠是在鏈表的最前面(頭插法)。
如果多線程同時put,如果同時觸發了Rehash擴容操作,會導致HashMap中的鏈表中出現循環節點,進而使得後面get的時候,會出現死循環,所以HashMap是非線程安全的。
4.2 get()
先定位到數組元素,再遍歷該元素處的鏈表,在尋找目標元素的時候,除了對比通過key計算出來的hash值,還會用雙等或equals方法對key本身來進行比較,兩者都爲true時纔會返回這個元素。
- 按照散列函數的定義,如果兩個對象相同,即
obj1.equals(obj2) = true
,則它們的hashCode必須相同;但如果兩個對象不同,則它們的hashCode不一定不同。 - 如果兩個不同對象的hashcode相同,就稱爲衝突。衝突會導致操作哈希表的時間開銷增大,所以覆蓋了equals方法之後一定要覆蓋hashCode方法。比如,
String a = new String(“abc”);
String b = new String(“abc”);
如果不覆蓋的話,那麼a和b的hashCode就會不同,把這兩個類當做key存到HashMap中的話就會出現問題,就會和key的唯一性相矛盾。
如何定位元素?即 二進制 hashCode & (leng-1)
- 計算"book"的hashcode
十進制 : 3029737
二進制 : 101110001110101110 1001- HashMap長度是默認的 16,length - 1 的結果
十進制 : 15
二進制 : 1111- 把以上兩個結果做與運算
101110001110101110 1001 & 1111 = 1001
1001的十進制 : 9,所以 index=9。
5. JDK1.8源碼分析
JDK1.7中的HashMap是由數組+鏈表+紅黑樹組成的,組成鏈表結點的是Node
包含三個元素:key
、value
和指向下一個Node的next
。
5.1 put()
- 判斷鍵值對數組table[i]是否爲空或爲null,如果爲空則創建Node;
- 根據鍵值key計算hash值得到插入的數組索引i,如果
table[i]==null
,直接新建節點添加,轉向⑥,如果table[i]不爲空,則轉向③; - 判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向④,這裏的相同指的是
hashCode
以及equals
; - 判斷table[i]是否爲
treeNode
紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤; - 遍歷table[i],判斷鏈表長度是否大於8,如果鏈表長度大於8的話把鏈表轉換爲紅黑樹,在紅黑樹中執行插入操作,否則進行鏈表的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;
- 插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量
threshold
,如果超過,進行resize()
擴容。
public V put(K key, V value) {
// 對key做hash運算得到hashCode
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。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 步驟③:節點key存在,直接覆蓋value。
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);
// 判斷鏈表長度是否大於8,如果鏈表長度大於8轉換爲紅黑樹進行處理。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果key已經存在並且equals相等,則直接覆蓋value。
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;
// 步驟⑥:超過最大容量就擴容。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
5.2 get()
- 判斷表是否爲空,並且計算索引位置,並對索引位置的值進行判空校驗。
- 如果表爲空 && 索引位置沒有值,直接返回null;
- 如果表不爲空 && 索引位置有值,執行步驟②。
- 判斷入參key與索引處第一個key的hashCode是否相等、 key是否相等或者equals是否相等。
- 如果步驟②條件滿足,則直接返回;否則執行步驟③;
- 判斷鏈接是否爲紅黑樹。
- 如果鏈表是紅黑樹,則按照紅黑樹二叉查找法獲取值;
- 如果不是紅黑樹(鏈表長度小於8爲普通鏈表),則遍歷鏈表,直到找到與入參key的hashCode相等、equals相等的key,並獲取該key的值。
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// Node不爲空 && 計算索引位置並且索引處有值
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 判斷key的hashCode是否相等
// && 判斷索引處第一個key與傳入key是否相等
// && 判斷key的equals是否相等。
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;
}
6. 使用示例
public class HashMapDemo {
@Test
public void test1() {
// 第一種:普通使用,二次取值
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0; i < 1000000; i++) {
map.put("test" + i, "test" + i);
}
long before = System.currentTimeMillis();
for (String key : map.keySet()) {
map.get(key);
}
long after = System.currentTimeMillis();
System.out.println("HashMap遍歷:" + (after - before) + "ms");
}
@Test
public void test2() {
// 推薦,尤其是容量大時
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0; i < 1000000; i++) {
map.put("test" + i, "test" + i);
}
long before = System.currentTimeMillis();
for (Map.Entry<String, Object> entry : map.entrySet()) {
entry.getValue();
entry.getKey();
}
long after = System.currentTimeMillis();
System.out.println("HashMap遍歷:" + (after - before) + "ms");
}
@Test
public void test3() {
// 通過Map.entrySet使用iterator遍歷key和value
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0; i < 1000000; i++) {
map.put("test" + i, "test" + i);
}
long before = System.currentTimeMillis();
Iterator<Entry<String, Object>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = (Entry<String, Object>) iterator.next();
entry.getKey();
entry.getValue();
}
long after = System.currentTimeMillis();
System.out.println("HashMap遍歷:" + (after - before) + "ms");
}
@Test
public void test4() {
// 通過Map.values()遍歷所有的value,但不能遍歷key
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0; i < 1000000; i++) {
map.put("test" + i, "test" + i);
}
long before = System.currentTimeMillis();
for (@SuppressWarnings("unused") Object object : map.values()) {
}
long after = System.currentTimeMillis();
System.out.println("HashMap遍歷:" + (after - before) + "ms");
}
}