java 8 HashMap 原始碼閱讀

local0發表於2021-09-09

閱讀java原始碼可能是每一個java程式設計師的必修課,只有知其所以然,才能更好的使用java,寫出更優美的程式,閱讀java原始碼也為我們後面閱讀java框架的原始碼打下了基礎。閱讀原始碼其實就像再看一篇長篇推理小說一樣,不能急於求成,需要慢慢品味才行。這一系列的文章,記錄了我閱讀原始碼的收穫與思路,讀者也可以借鑑一下,也僅僅是借鑑,問渠那得清如許,絕知此事要躬行!要想真正的成為大神,還是需要自己親身去閱讀原始碼而不是看幾篇分析原始碼的部落格就可以的。

正文

HashMap是我們經常用的的一個集合類,其中java對於Hash雜湊表的維護、大小的動態擴充套件以及解決Hash衝突的方法都是值得我們借鑑的。如何更好地使用HashMap,建議大家把JAVA API文件拿來讀讀,其中對於如何很好的使用HashMap做了詳細的說明,在一個是將HashMap的原始碼自行分析一遍

總結

JAVA8中對HashMap的最佳化

透過閱讀原始碼,我們可以瞭解到,在java1.8這個版本中,SUN大神們為hashmap的查詢進行了進一步最佳化,原來hashmap是hash表+連結串列的形式,在1.8中變為了hash表+連結串列/樹的形式,即在一定條件下同一hash值對應的連結串列會被轉化為樹,進而最佳化了查詢。透過這次學習Hashmap的原始碼實現,我們可以學習到如何利用樹來對陣列查詢進行最佳化。那麼何時樹化?何時調整表的大小?
在HashMap的類成員中,有一個叫做MIN_TREEIFY_CAPACITY的常量,它規定了當HashMap被使用的空間大小超過這個常量的值時,才會開始樹化。而針對每一個hash值對應的連結串列,有一個叫TREEIFY_THRESHOLD
常量,規定了當連結串列的大小超過其時,就對此連結串列進行樹化。

原始碼分析

HashMap關鍵的變數:

/**
     * The default initial capacity - MUST be a power of two.
     *
     *   HashMap中雜湊表的初始容量預設值
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 [] table;

    /**
     * The number of key-value mappings contained in this map.
     *  Hashmap當前的大小
     */
    transient int size;

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     * 
     */
    transient int modCount;

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    //閥值,它決定了hashmap何時進行擴容。
    int threshold;

    /**
     * The load factor for the hash table.
     *  負載因子,用於計算閥值,它等於threshold與hashmap當前容量的比例。
     * @serial
     */
    final float loadFactor;

再介紹了hashmap的重要變數之後,我們就可以看看其最關鍵的put()方法與resize()方法了:
put():

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;

        //如果還沒有為hash表申請空間,那麼就使用resize()方法初始化hash表。
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //如果沒有發生hash衝突,則直接將資料存入hash表中
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //如果發生了衝突
        else {
            Node e; K k;
            //判斷是否與連結串列頭結點相同
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //判斷當前要插入的連結串列表示的結構是否是樹,如果是則交給putTreeVal()處理
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);

           //當要插入的資料不是頭結點,並且連結串列沒有被樹化的情況下
            else {
                //遍歷連結串列,判斷是做修改還是插入操作
                for (int binCount = 0; ; ++binCount) {
                    //如果是插入操作
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //判斷是否達到樹化的標準
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);         //樹化連結串列
                        break;
                    }
                    //發現當前連結串列中的結點有與要插入的資料相同的key,跳出迴圈進行修改操作
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //修改操作,,進行Value資料的修改
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //此方法是在LinkedHashMap中實現的,在HashMap中為空
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //判斷put()後是否需要擴容
        if (++size > threshold)
            resize();
         //此方法是在LinkedHashMap中實現的,在HashMap中為空
        afterNodeInsertion(evict);
        return null;
    }

這裡需要注意幾點:
1.為什麼在查詢插入資料在hash表中相應位置時,使用的是hash(key)&(length-1)而不是hash(key)?
因為hash(key)的值是隨機的,無法確定其範圍,透過&操作,相當於對hash表的長度取模,能夠在保證資料隨機均勻的分佈在hash表中,並且限制hash值的範圍。

2.因為連結串列的存在,所以理論上hashmap的容量是沒有上限的,但是當hash表無法繼續擴充時,隨著儲存資料的增加,其查詢效率會逐漸降低。
3.負載因子的作用:負載因子,其實表達的都是HashMap容量空間的佔有程度,它存在的意義是為了協調查詢效率與空間利用率之間的平衡。Capacity*loadFactor=threshold,threshold其實就表示了hashmap的真實容量大小,而Capacity則是hash表的長度。負載因子越大則代表容量空間的佔有程度高,也就是能容納更多的元素,元素多了,連結串列大了,所以此時查詢效率就會降低。反之,負載因子越小則連結串列中的資料量就越稀疏,此時會對空間造成爛費,但是此時查詢效率高。

resize():

 final Node[] resize() {
        Node[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //先確定新表的hash表長度與閥值。
        if (oldCap > 0) {
            //判斷當前hash表的長度是否已經達到上限,如果是則將閥值設定為Integer.MAX_VALUE並返回
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //將容量大小增加為原來的二倍,並計算相應的閥值
            else if ((newCap = oldCap = DEFAULT_INITIAL_CAPACITY)
                newThr = 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 [] newTab = (Node[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j  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)e).split(this, newTab, j, oldCap);
                    //非樹化連結串列的情況
                    else { // preserve order
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node next;

                        //這裡的程式碼需要好好看,就是因為此處實現方式的原因,
                       //導致了hashmap遍歷時不能保證資料能夠一直按照插入或者修改的順序訪問。
                        do {
                            next = e.next;
                            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;
    }

關於resize需要注意的是:
1.resize擴容後的容量是原來的兩倍,直到容量達到最大,這時就會更改閾值來繼續擴容。
2.正是因為resize的擴容原理,導致了Hashmap不能保證插入資料的順序性,當然如果一定要保證,我們可以使用LinkedHashMap

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/132/viewspace-2799882/,如需轉載,請註明出處,否則將追究法律責任。

相關文章