在JDK1.8以前HashMap底層數據結構爲數組加鏈表,table爲其內部類Entry的數組,而Entry實現是一個鏈表的數據結構;JDK1.8 HashMap底層數據結構爲數組加鏈表與紅黑樹,table爲Entry的數組,但entry有 Node與TreeNode兩種實現,Node爲鏈表實現,TreeNode紅黑樹實現,當結點個數沒超過8個時,桶裏面爲Node結點構成的鏈表,若是超過桶裏面爲TreeNode結點構成的紅黑樹。JDK1.8 putVal方法源碼以下:html
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //判斷table是否被初始化沒有時調用resize()方法初始化 if ((tab = table) == null || (n = tab.length) == 0) 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; //新放入桶中的元素原來已存在 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //新放入桶中的元素原來不存在,且桶裏面原先結構爲TreeNode構成的紅黑樹 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //新放入桶中的元素原來不存在,且桶裏面原先結構爲Node構成鏈表 else { //遍歷桶中的Node構成鏈表,從頭向尾遍歷 for (int binCount = 0; ; ++binCount) { //遍歷到最後一下結點 if ((e = p.next) == null) { //將新放入的結點加入鏈表尾部 p.next = newNode(hash, key, value, null); //若是Node構成鏈表的長度超過8個,將桶中Node構成鏈表變爲TreeNode構成的紅黑樹 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //放入的key在Node構成鏈表已存在 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; }
從putVal如下代碼,可看出當桶中爲Node結點構成的鏈表時,新放入的元素是加到鏈表的尾部的,這與JDK1.8以前的版本不太同樣;JDK1.8以前的版本新放入的元素是加在鏈表的頭部的。(前陣子去餓了麼面試面試官問我新加入的元素是放在尾部仍是頭部,我回答尾部,面試官說不對,這個實際是要區分JDK版本的,很細節的一個問題,估計我之後當面試官會問這個問題^-^)面試
//遍歷桶中的Node構成鏈表,從頭向尾遍歷 for (int binCount = 0; ; ++binCount) { //遍歷到最後一下結點 if ((e = p.next) == null) { //將新放入的結點加入鏈表尾部 p.next = newNode(hash, key, value, null); //若是Node構成鏈表的長度超過8個,將桶中Node構成鏈表變爲TreeNode構成的紅黑樹 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //放入的key在Node構成鏈表已存在 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } ``` JDK1.7 實現put方法實現 ``` public V put(K paramK, V paramV) { //table爲空根據閾值初始化table if (this.table == EMPTY_TABLE) inflateTable(this.threshold); //key爲null時特殊處理 if (paramK == null) return putForNullKey(paramV); int i = hash(paramK); int j = indexFor(i, this.table.length); //遍歷具體桶中的鏈表 for (Entry localEntry = this.table[j]; localEntry != null; localEntry = localEntry.next) { Object localObject1; //若是新要放入的key在原連接中已存在 if ((localEntry.hash == i) && (((localObject1 = localEntry.key) == paramK) || (paramK.equals(localObject1)))) { Object localObject2 = localEntry.value; localEntry.value = paramV; localEntry.recordAccess(this); return localObject2; } } this.modCount += 1; //加入新key到桶中 addEntry(i, paramK, paramV, j); return null; } ``` 再來看看addEnty方法與createEntry方法實現 ``` void addEntry(int paramInt1, K paramK, V paramV, int paramInt2) { //擴容處理 if ((this.size >= this.threshold) && (null != this.table[paramInt2])) { resize(2 * this.table.length); paramInt1 = null != paramK ? hash(paramK) : 0; paramInt2 = indexFor(paramInt1, this.table.length); } //加入新結點 createEntry(paramInt1, paramK, paramV, paramInt2); } void createEntry(int paramInt1, K paramK, V paramV, int paramInt2) { //保存桶中第一個結點 Entry localEntry = this.table[paramInt2]; //原第一個結點對應鏈表接到新加入結點尾部同時更新桶中第一個結點 this.table[paramInt2] = new Entry(paramInt1, paramK, paramV, localEntry); this.size += 1; } ``` 另外還有一點JDK1.7 HashMap在併發時因爲resize可能致使桶中的鏈表出現循環鏈的狀況,具體分析能夠參考[https://coolshell.cn/articles/9606.html](http://) JDK1.8 應該不會出現這個問題,只是本人猜測後面我會分析一下具體源碼。先到這裏下面分析在併發狀況下JDK1.8版本的HashMap到底會不會出現循環鏈的狀況。