JAVA7/8中的HashMap
阿新 • • 發佈:2018-12-25
Java7 HashMap
HashMap 是最簡單的,一來我們非常熟悉,二來就是它不支援併發操作,所以原始碼也非常簡單。
首先,我們用下面這張圖來介紹 HashMap 的結構。
如圖,HashMap 裡面是一個數組,陣列中每個元素是一個單向連結串列,每個元素對應一個Entry,通過next指向下一個Entry。
put 過程分析
public V put(K var1, V var2) { // 當插入第一個元素的時候,需要先初始化陣列大小 if(this.table == EMPTY_TABLE) { this.inflateTable(this.獲取key的hash及計算下標的程式碼此處不貼,主要看下對key是null值的處理,其最終會放到table[0]處threshold); } if(var1 == null) { //key 是null時執行 最終會放到table[0]中 return this.putForNullKey(var2); } else { int var3 = this.hash(var1);//對key進行hash獲取hash值 int var4 = indexFor(var3, this.table.length);//通過hash值獲取將要放到table的下標index for(HashMap.Entry var5 = this.table[var4]; var5 != null; var5 = var5.next) {//遍歷當前下標的entry鏈 如果key已存在 則set新的value 並將舊的value返回 if(var5.hash == var3) { Object var6 = var5.key; if(var5.key == var1 || var1.equals(var6)) { Object var7 = var5.value; var5.value = var2; var5.recordAccess(this); return var7; } } } ++this.modCount; this.addEntry(var3, var1, var2, var4);//key不存在新增新的entry到連結串列中 return null; } }
private V putForNullKey(V var1) { for(HashMap.Entry var2 = this.table[0]; var2 != null; var2 = var2.next) {//key相同 直接替換 並返回舊值 所以key是null的entry只能有一個! if(var2.key == null) { Object var3 = var2.value; var2.value = var1; var2.recordAccess(this); return var3; } } ++this.modCount; this.addEntry(0, (Object)null, var1, 0);//table中不存在key 則新增新的Entry return null; }
新增Entry的方法,先判斷是否須要擴容,這裡也可以很清晰的看到新的Entry會放到連結串列的頭部
void addEntry(int var1, K var2, V var3, int var4) { if(this.size >= this.threshold && null != this.table[var4]) { // 如果當前 HashMap 大小已經達到了閾值,並且新值要插入的陣列位置已經有元素了,那麼要擴容 this.resize(2 * this.table.length); var1 = null != var2?this.hash(var2):0; var4 = indexFor(var1, this.table.length); } this.createEntry(var1, var2, var3, var4);//新增新的entry } void createEntry(int var1, K var2, V var3, int var4) { HashMap.Entry var5 = this.table[var4];//原位置被新的entry替換,舊的entry作為其next 形成新的連結串列結構 this.table[var4] = new HashMap.Entry(var1, var2, var3, var5); ++this.size; }
get 過程分析
public V get(Object var1) { if(var1 == null) { return this.getForNullKey(); } else { HashMap.Entry var2 = this.getEntry(var1); return null == var2?null:var2.getValue(); } } final HashMap.Entry<K, V> getEntry(Object var1) { if(this.size == 0) { return null; } else { int var2 = var1 == null?0:this.hash(var1); // 找到對應下標的連結串列,key相同時返回值 for(HashMap.Entry var3 = this.table[indexFor(var2, this.table.length)]; var3 != null; var3 = var3.next) { if(var3.hash == var2) { Object var4 = var3.key; if(var3.key == var1 || var1 != null && var1.equals(var4)) { return var3; } } } return null; } }
JDK8中的HashMap
Java8 對 HashMap 進行了一些修改,最大的不同就是利用了紅黑樹,所以其由 陣列+連結串列+紅黑樹 組成。
根據 Java7 HashMap 的介紹,我們知道,查詢的時候,根據 hash 值我們能夠快速定位到陣列的具體下標,但是之後的話,需要順著連結串列一個個比較下去才能找到我們需要的,時間複雜度取決於連結串列的長度,為 O(n)。為了降低這部分的開銷,在 Java8 中,當連結串列中的元素超過了 8 個以後,會將連結串列轉換為紅黑樹,在這些位置進行查詢的時候可以降低時間複雜度為 O(logN)。
Java7 中使用 Entry 來代表每個 HashMap 中的資料節點,Java8 中使用 Node,基本沒有區別,都是 key,value,hash 和 next 這四個屬性,不過,Node 只能用於連結串列的情況,紅黑樹的情況需要使用 TreeNode。
put 過程分析
public V put(K key, V value) { 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; // 第一次 put 值的時候,會觸發下面的 resize(),類似 java7 的第一次 put 也要初始化陣列長度 // 第一次 resize 和後續的擴容有些不一樣,因為這次是陣列從 null 初始化到預設的 16 或自定義的初始容量 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 找到具體的陣列下標,如果此位置沒有值,那麼直接初始化一下 Node 並放置在這個位置就可以了 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else {// 陣列該位置有資料 Node<K,V> e; K k; // 首先,判斷該位置的第一個資料和我們要插入的資料,key 是不是"相等",如果是,取出這個節點 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) { // 插入到連結串列的最後面(Java7 是插入到連結串列的最前面) if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // TREEIFY_THRESHOLD 為 8,所以,如果新插入的值是連結串列中的第 9 個 // 會觸發下面的 treeifyBin,也就是將連結串列轉換為紅黑樹 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 如果在該連結串列中找到了"相等"的 key(== 或 equals) if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; // 此時 break,那麼 e 為連結串列中[與要插入的新值的 key "相等"]的 node p = e; } } // e!=null 說明存在舊值的key與要插入的key"相等" // 對於我們分析的put操作,下面這個 if 其實就是進行 "值覆蓋",然後返回舊值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) //onlyIfAbsent為true的時候如果key存在則不替換存在的舊值 預設為false e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 如果 HashMap 由於新插入這個值導致 size 已經超過了閾值,需要進行擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); re
總的來說,單就PUT而言 ,JAVA8對HashMap的調整在兩個方面,一是當連結串列中的元素超過了 8 個以後,會將連結串列轉換為紅黑樹
二是新的鍵值對會插入到連結串列尾部而不是頭部。