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
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 }
思路以下: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 }
實現思路:
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對應的哈希與高位運算結果。
元素在從新計算hash以後,由於n變爲2倍,那麼n-1的mask範圍在高位多1bit(紅色),所以新的index就會發生這樣的變化:
所以,咱們在擴充HashMap的時候,不須要像JDK1.7的實現那樣從新計算hash,只須要看看原來的hash值新增的那個bit是1仍是0就行了,是0的話索引沒變,是1的話索引變成「原索引+oldCap」,能夠看看下圖爲16擴充爲32的resize示意圖:
這個設計確實很是的巧妙,既省去了從新計算hash值的時間,並且同時,因爲新增的1bit是0仍是1能夠認爲是隨機的,所以resize的過程,均勻的把以前的衝突的節點分散到新的bucket了。這一塊就是JDK1.8新增的優化點。有一點注意區別,JDK1.7中rehash的時候,舊鏈表遷移新鏈表的時候,若是在新表的數組索引位置相同,則鏈表元素會倒置,可是從上圖能夠看出,JDK1.8不會倒置。