HashMap JDK1.8實現原理

HashMap概述

HashMap存儲的是key-value的鍵值對,容許key爲null,也容許value爲null。HashMap內部爲數組+鏈表的結構,會根據key的hashCode值來肯定數組的索引(確認放在哪一個桶裏),若是遇到索引相同的key,桶的大小是2,若是一個key的hashCode是7,一個key的hashCode是3,那麼他們就會被分到一個桶中(hash衝突),若是發生hash衝突,HashMap會將同一個桶中的數據以鏈表的形式存儲,可是若是發生hash衝突的機率比較高,就會致使同一個桶中的鏈表長度過長,遍歷效率下降,因此在JDK1.8中若是鏈表長度到達閥值(默認是8),就會將鏈表轉換成紅黑二叉樹。node

HashMap數據結構

 1    
 2     //Node本質上是一個Map.存儲着key-value
 3     static class Node<K,V> implements Map.Entry<K,V> {
 4         final int hash;             //保存該桶的hash值
 5         final K key;                //不可變的key
 6         V value;                    
 7         Node<K,V> next;      //指向一個數據的指針
 8 
 9         Node(int hash, K key, V value, Node<K,V> next) {
10             this.hash = hash;
11             this.key = key;
12             this.value = value;
13             this.next = next;
14         }        

從源碼上能夠看到,Node實現了Map.Entry接口,本質上是一個映射(k-v)算法

剛剛也說過了,有時候兩個key的hashCode可能會定位到一個桶中,這時就發生了hash衝突,若是HashMap的hash算法越散列,那麼發生hash衝突的機率越低,若是數組越大,那麼發生hash衝突的機率也會越低,可是數組越大帶來的空間開銷越多,可是遍歷速度越快,這就要在空間和時間上進行權衡,這就要看看HashMap的擴容機制,在說擴容機制以前先看幾個比較重要的字段數組

 1 //默認桶16個
 2 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
 3 
 4 //默認桶最多有2^30個
 5 static final int MAXIMUM_CAPACITY = 1 << 30;
 6 
 7 //默認負載因子是0.75
 8 static final float DEFAULT_LOAD_FACTOR = 0.75f;
 9 
10 //能容納最多key_value對的個數
11  int threshold;
12 
13 //一共key_value對個數
14 int size;

threshold=負載因子 * length,也就是說數組長度固定之後, 若是負載因子越大,所能容納的元素個數越多,若是超過這個值就會進行擴容(默認是擴容爲原來的2倍),0.75這個值是權衡過空間和時間得出的,建議你們不要隨意修改,若是在一些特殊狀況下,好比空間比較多,但要求速度比較快,這時候就能夠把擴容因子調小以較少hash衝突的機率。相反就增大擴容因子(這個值能夠大於1)。數據結構

size就是HashMap中鍵值對的總個數。還有一個字段是modCount,記錄是發生內部結構變化的次數,若是put值,可是put的值是覆蓋原有的值,這樣是不算內部結構變化的。app

 由於HashMap擴容每次都是擴容爲原來的2倍,因此length老是2的次方,這是很是規的設置,常規設置是把桶的大小設置爲素數,由於素數發生hash衝突的機率要小於合數,好比HashTable的默認值設置爲11,就是桶的大小爲素數的應用(HashTable擴容後不能保證是素數)。HashMap採用這種設置是爲了在取模和擴容的時候作出優化。ide

hashMap是經過key的hashCode的高16位和低16位異或後和桶的數量取模獲得索引位置,即key.hashcode()^(hashcode>>>16)%length,當length是2^n時,h&(length-1)運算等價於h%length,而&操做比%效率更高。而採用高16位和低16位進行異或,也可讓全部的位數都參與越算,使得在length比較小的時候也能夠作到儘可能的散列。優化

在擴容的時候,若是length每次是2^n,那麼從新計算出來的索引只有兩種狀況,一種是 old索引+16,另外一種是索引不變,因此就不須要每次都從新計算索引。 this

 

肯定哈希桶數據索引位置

 1 //方法一:
 2 static final int hash(Object key) {   //jdk1.8 & jdk1.7
 3      int h;
 4      // h = key.hashCode() 爲第一步 取hashCode值
 5      // h ^ (h >>> 16)  爲第二步 高位參與運算
 6      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 7 }
 8 //方法二:
 9 static int indexFor(int h, int length) {  //jdk1.7的源碼,jdk1.8沒有這個方法,可是實現原理同樣的
10      return h & (length-1);  //第三步 取模運算
11 }

 

HashMap的put方法實現

 思路以下:spa

1.table[]是否爲空設計

2.判斷table[i]處是否插入過值

3.判斷鏈表長度是否大於8,若是大於就轉換爲紅黑二叉樹,並插入樹中

4.判斷key是否和原有key相同,若是相同就覆蓋原有key的value,並返回原有value

5.若是key不相同,就插入一個key,記錄結構變化一次

 1  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 2                    boolean evict) {
 3 //判斷table是否爲空,若是是空的就建立一個table,並獲取他的長度
 4         Node<K,V>[] tab; Node<K,V> p; int n, i;
 5         if ((tab = table) == null || (n = tab.length) == 0)
 6             n = (tab = resize()).length;
 7 //若是計算出來的索引位置以前沒有放過數據,就直接放入
 8         if ((p = tab[i = (n - 1) & hash]) == null)
 9             tab[i] = newNode(hash, key, value, null);
10         else {
11 //進入這裏說明索引位置已經放入過數據了
12             Node<K,V> e; K k;
13 //判斷put的數據和以前的數據是否重複
14             if (p.hash == hash &&
15                 ((k = p.key) == key || (key != null && key.equals(k))))   //key的地址或key的equals()只要有一個相等就認爲key重複了,就直接覆蓋原來key的value
16                 e = p;
17 //判斷是不是紅黑樹,若是是紅黑樹就直接插入樹中
18             else if (p instanceof TreeNode)
19                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
20             else {
21 //若是不是紅黑樹,就遍歷每一個節點,判斷鏈表長度是否大於8,若是大於就轉換爲紅黑樹
22                 for (int binCount = 0; ; ++binCount) {
23                     if ((e = p.next) == null) {
24                         p.next = newNode(hash, key, value, null);
25                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
26                             treeifyBin(tab, hash);
27                         break;
28                     }
29 //判斷索引每一個元素的key是否可要插入的key相同,若是相同就直接覆蓋
30                     if (e.hash == hash &&
31                         ((k = e.key) == key || (key != null && key.equals(k))))
32                         break;
33                     p = e;
34                 }
35             }
36 //若是e不是null,說明沒有迭代到最後就跳出了循環,說明鏈表中有相同的key,所以只須要將value覆蓋,並將oldValue返回便可
37             if (e != null) { // existing mapping for key
38                 V oldValue = e.value;
39                 if (!onlyIfAbsent || oldValue == null)
40                     e.value = value;
41                 afterNodeAccess(e);
42                 return oldValue;
43             }
44         }
45 //說明沒有key相同,所以要插入一個key-value,並記錄內部結構變化次數
46         ++modCount;
47         if (++size > threshold)
48             resize();
49         afterNodeInsertion(evict);
50         return null;
51     }
View Code

 

HashMap的get方法實現

 實現思路:

1.判斷表或key是不是null,若是是直接返回null

2.判斷索引處第一個key與傳入key是否相等,若是相等直接返回

3.若是不相等,判斷鏈表是不是紅黑二叉樹,若是是,直接從樹中取值

4.若是不是樹,就遍歷鏈表查找

 1   final Node<K,V> getNode(int hash, Object key) {
 2         Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
 3 //若是表不是空的,而且要查找索引處有值,就判斷位於第一個的key是不是要查找的key
 4         if ((tab = table) != null && (n = tab.length) > 0 &&
 5             (first = tab[(n - 1) & hash]) != null) {
 6             if (first.hash == hash && // always check first node
 7                 ((k = first.key) == key || (key != null && key.equals(k))))
 8 //若是是,就直接返回
 9                 return first;
10 //若是不是就判斷鏈表是不是紅黑二叉樹,若是是,就從樹中取值
11             if ((e = first.next) != null) {
12                 if (first instanceof TreeNode)
13                     return ((TreeNode<K,V>)first).getTreeNode(hash, key);
14 //若是不是樹,就遍歷鏈表
15                 do {
16                     if (e.hash == hash &&
17                         ((k = e.key) == key || (key != null && key.equals(k))))
18                         return e;
19                 } while ((e = e.next) != null);
20             }
21         }
22         return null;
23     }

 

擴容機制

 

咱們使用的是2次冪的擴展(指長度擴爲原來2倍),因此,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。看下圖能夠明白這句話的意思,n爲table的長度,圖(a)表示擴容前的key1和key2兩種key肯定索引位置的示例,圖(b)表示擴容後key1和key2兩種key肯定索引位置的示例,其中hash1是key1對應的哈希與高位運算結果。

hashMap 1.8 哈希算法例圖1

元素在從新計算hash以後,由於n變爲2倍,那麼n-1的mask範圍在高位多1bit(紅色),所以新的index就會發生這樣的變化:

hashMap 1.8 哈希算法例圖2

所以,咱們在擴充HashMap的時候,不須要像JDK1.7的實現那樣從新計算hash,只須要看看原來的hash值新增的那個bit是1仍是0就行了,是0的話索引沒變,是1的話索引變成「原索引+oldCap」,能夠看看下圖爲16擴充爲32的resize示意圖:

jdk1.8 hashMap擴容例圖

這個設計確實很是的巧妙,既省去了從新計算hash值的時間,並且同時,因爲新增的1bit是0仍是1能夠認爲是隨機的,所以resize的過程,均勻的把以前的衝突的節點分散到新的bucket了。這一塊就是JDK1.8新增的優化點。有一點注意區別,JDK1.7中rehash的時候,舊鏈表遷移新鏈表的時候,若是在新表的數組索引位置相同,則鏈表元素會倒置,可是從上圖能夠看出,JDK1.8不會倒置。

相關文章
相關標籤/搜索