1. 程式人生 > >JAVA7/8中的HashMap

JAVA7/8中的HashMap

Java7 HashMap

HashMap 是最簡單的,一來我們非常熟悉,二來就是它不支援併發操作,所以原始碼也非常簡單。

首先,我們用下面這張圖來介紹 HashMap 的結構。



如圖,HashMap 裡面是一個數組,陣列中每個元素是一個單向連結串列,每個元素對應一個Entry,通過next指向下一個Entry。

put 過程分析

public V put(K var1, V var2) {
        // 當插入第一個元素的時候,需要先初始化陣列大小
        if(this.table == EMPTY_TABLE) {
            this.inflateTable(this.
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; } }
獲取key的hash及計算下標的程式碼此處不貼,主要看下對key是null值的處理,其最終會放到table[0]處
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 個以後,會將連結串列轉換為紅黑樹

二是新的鍵值對會插入到連結串列尾部而不是頭部。