一、摘要
以下分析內容均是基於JDK1.8產生的,同時也和JDK1.7版本的hashmap做了一些比較。在1.7版本中,HashMap的實現是基於數組+鏈表的形式,而在1.8版本中則引入了紅黑樹,但其實好多內容都是相同的。

從上面圖中可以看出,HashMap等於數組+鏈表+紅黑樹三者結合。當進來的數據被Hash后會得到一個數組的下標,從而可以找到對應的位置,當該數組元素存在元素時,則會相應的以鏈表的形式給出,同時我們想取出value值時也要相應對key進行equals才能找到相應的位置,當鏈表長度大於8時,則會轉換成紅黑樹來表示。
二、源碼分析
1、HashMap主要的成員值:
//源碼英文注釋均舍去 //初始化Node數組容量16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //初始化最大的數組容量 static final int MAXIMUM_CAPACITY = 1 << 30; //初始化負載因子0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; //由鏈表轉紅黑樹的臨界值 static final int TREEIFY_THRESHOLD = 8; //由紅黑樹轉鏈表的臨界值 static final int UNTREEIFY_THRESHOLD = 6; //桶可能被轉化為樹形結構的最小容量的臨界值 static final int MIN_TREEIFY_CAPACITY = 64; //計數器 transient int modCount; //Node數組擴容的臨界值,第一次為12 int threshold;
2、HashMap主要的構造方法
HashMap中有四個構造方法,但這四個構造方法主要的目的還是在於初始化數組的容量以及負載因子(這個變量涉及到數組的擴容的問題),下面僅拿一個構造函數來講:
1 public HashMap(int initialCapacity, float loadFactor) { 2 //判斷初始化數組的容量大小 3 if (initialCapacity < 0) 4 throw new IllegalArgumentException("Illegal initial capacity: " + 5 initialCapacity); 6 if (initialCapacity > MAXIMUM_CAPACITY) 7 initialCapacity = MAXIMUM_CAPACITY; 8 //判斷初始化的負載因子 9 if (loadFactor <= 0 || Float.isNaN(loadFactor)) 10 throw new IllegalArgumentException("Illegal load factor: " + 11 loadFactor); 12 //初始化負載因子 13 this.loadFactor = loadFactor; 14 this.threshold = tableSizeFor(initialCapacity); 15 }
3、HashMap中主要的方法分析
a、putVal()方法
當我們使用map.put時,該方法會去調用putVal()方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) //會對該桶進行第一次初始化,桶的數組大小為16 n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) //判斷桶的下標是否含有第一個元素,沒有的話就放進去 tab[i] = newNode(hash, key, value, null); else { //桶的下標已經存在第一個元素了 Node<K,V> e; K k; //判斷桶下標中存在的第一個元素的hash值和key值是否相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //相等的話則用e來進行記錄 e = p; else if (p instanceof TreeNode) //hash值相等,key不相等則判斷標中存在的第一個元素是否為樹的節點 //是的話則將元素添加到樹節點上 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //hash值相等,key不相等放到鏈表中 else { for (int binCount = 0; ; ++binCount) { //判斷該鏈表尾部指針是不是空的 if ((e = p.next) == null) { //在鏈表的尾部創建鏈表節點 p.next = newNode(hash, key, value, null); //判斷鏈表的長度是否達到轉化紅黑樹的臨界值,臨界值為8 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; } } //判斷當前的key已經存在的情況下,再來一個相同的hash值、key值時返回新來的value這個值 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; }
b、resize()方法
在putVal()中,我們看到在這個函數里面使用到了2次resize()方法,resize()方法表示的在進行第一次初始化時會對其進行擴容,或者當該數組的實際大小大於其臨界值值(第一次為12),這個時候在擴容的同時也會伴隨的桶上面的元素進行重新分發,這也是JDK1.8版本的一個優化的地方,在1.7中,擴容之后需要重新去計算其Hash值,根據Hash值對其進行分發,但在1.8版本中,則是根據在同一個桶的位置中進行判斷(e.hash & oldCap)是否為0,重新進行hash分配后,該元素的位置要么停留在原始位置,要么移動到原始位置+增加的數組大小這個位置上
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; //判斷舊的table大小 if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //上面這些均是代表對桶的大小進行一些判斷並初始化 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; //擴容之后對舊的桶進行重新分配,打散到其他的位置,使其均勻的分散 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; //將同一桶中的元素根據(e.hash & oldCap)是否為0進行分割 //為0的話則保留在原始的位置 //不為0的話則將其移動到原始位置+增加的數組大小(比如第二次擴容時,這時值就為16)這個位置上面 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
c、treeifyBin()方法
在putVal()方法中,我們能夠看到,當鏈表的長度大於TREEIFY_THRESHOLD這個臨界值時,這個時候就會調用treeifyBin()方法,將鏈表的結構轉化為紅黑樹結構,這也是JDK1.8版本新優化的功能點
在此方法中主要做了:
1、判斷桶是否初始化、或者判斷桶中的元素個數是否達到MIN_TREEIFY_CAPACITY閾值,沒有的話則去進行初始化或者擴容
2、若不符合上述條件,則會對其進行樹形化,首先會先去遍歷桶中鏈表的元素,並創建相同的樹節點,接着會根據桶的第一個元素而去創建樹的頭結點,並以此建立聯系
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); //開始樹形化 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; //對桶Node中的鏈表元素進行循環,從鏈表的頭節點開始將鏈表的頭元素改為樹的頭節點 do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { //樹的頭節點不為空時 p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); //將桶中的元素與樹的頭節點進行連接 if ((tab[index] = hd) != null) hd.treeify(tab); } }
三、細節注意
1、在上述方法中,我們經常看到在進行當前元素是否相同時會去進行判斷,如果僅僅是對值的hashCode進行判斷,當hash值相同時,則會發生Hash碰撞,這個時候利用鏈表的形式去解決hash碰撞的問題,當碰撞發生了,則會將元素存放在鏈表的下一個節點中,同時在判斷兩個是否是同一個元素時,需要去判斷當且僅當hashCode()和equal都相等時才能判斷這兩個元素是相等的,兩元素相同時則會用新的value替換掉舊的value值
2、在對桶進行擴容時,當桶的實際使用大小超多了0.75*桶的容量時,這個時候要對其進行擴容,同時擴容之后原桶上的元素的位置也會從新被打散,其判斷條件是通過值的hash與上原始的容量,若等於0則停留在原始的位置不動,若等於1則新的位置=原始的位置+新增了多少個數組
3、當鏈表的長度大於8時,這個時候就需要將鏈表樹形化轉換成紅黑樹
4、根據(n - 1) & hash來判斷桶的數組大小最好是2的冪次方,如果length不是2的次冪,比如length為15,則length-1為14,對應的二進制為1110,在於h與操作,最后一位都為0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了
