java基礎提升之HashMap

        這一篇開始,咱們進入集合中map的學習。咱們首先從HashMap開始。java

源碼中的介紹

        同樣的套路,同樣的開始,咱們從HashMap源碼中介紹開始,值得一說的是,筆者使用的是8版本的jdk,因此一下是jdb1.8中HashMap的介紹。數組

  1. 基於哈希表的map接口實現,這個實現停工了map的全部可選操做,而且容許null的value以及null的key。HashMap類大體至關於Hashtable除了他不是同步的以及容許null。這個類對他其中存儲的元素的順序沒有保證,特別的是,他不保證順序隨着時間的推移保持不變。數據結構

  2. 假定散列函在桶之間正確的分散元素則HashMap能提供恆定時間性能表現對於基礎操做:get、put。對此集合進行迭代操做所須要的時間與這個HashMap實例的容量加上這個HashMap的鍵值對數量成正比,因此若是迭代性能很重要的狀況下,不要講初始容量設置的過高或者將負載因子設置的過低。併發

  3. 兩個參數影響HashMap的性能,初始容量以及負載因子,容量是指哈希表的桶的容量,在下面中咱們會講解什麼是桶。初始容量就簡單的值哈希表建立的時候的容量。負載因子則是一個度量表明哈希表多滿的時候進行擴容。當哈希表中entries的數量即鍵值對的數量大於當前容量與負載因子的乘積時,哈希表進行rehashed,即重建內部數據結構,擴容後的桶數量時擴容前的兩倍。app

  4. 默認的增加因子的數值爲0.75。這個數值在時間與空間上作了一個很好的權衡,更高的值會增長空間利用率,可是會下降查詢性能。函數

  5. 若是須要存儲不少數量的鍵值對,那麼建立一個初始容量大的哈希表所表現出來的性能要比讓其自動擴容到須要的大小的哈希表的性能好。固然,使用不少hashcode同樣的key一樣也會下降HashMap的性能。性能

  6. HashMap不是同步的,當有多個線程併發讀取而且至少有一個線程對其進行告終構更改,那麼必需要作額外的加鎖操做。可使用如下方法進行加鎖: Map m = Collections.synchronizedMap(new HashMap(...));學習

  7. 這個類中返回的全部迭代器(Iterator)都是快速失敗機制的。若是在迭代器建立後,這個map結構發生改變,則調用iterator的next與hasNext方法會盡最大努力拋出ConcurrentModificationException。所以在面對併發修改時,迭代器乾淨而快速的失敗。this

  8. 值得一說的是,在這個版本中HashMap中加入了紅黑樹,當一個桶中鍵值對的數量大於8時,會進行樹化操做。咱們會在下面的章節中講解。spa

存儲結構

HashMap在未樹化前的存儲結構以下圖:

由圖能夠看出,存儲結構是數組+鏈表的形式。

HashMap樹化後的存儲結構以下圖:

能夠看出,數組中存儲的是每棵樹的root。

如今咱們來看一下兩種不一樣的存儲結構的節點源碼:

這個是未樹化前的節點結構

這個是樹化後的節點結構

爲何要引入紅黑樹

如今我們來探討一下,爲何java8要引入紅黑樹,咱們仔細觀察未樹化前的HashMap的存儲結構,當咱們有許多hashCode相同的key要插入到HashMap中時會發生什麼狀況呢? 咱們知道,HashMap在存儲的時候會根據hashcode去計算出數組中的索引,而後將這個鍵值對插入到數組這個索引的鏈表裏。咱們再來想一下HashMap的查詢順序,咱們須要先計算出hashcode定位到數組的某一個桶即某一個索引。以後咱們迭代查詢對比key,以後返回結果。因此,當有大量hashcode相同的key插入到紅黑樹中時,這個鏈表就會變得特別長,長到HashMap的查找操做幾乎變成線性的,以下圖:

這種狀況下,查詢的速度將堆大大下降,因此在此引入了樹的概念,那麼爲何是紅黑樹呢?由於直接使用二叉搜索樹的話也會出現極端狀況,以下:

這樣致使的結果依然改善hashcode碰撞概率大狀況下,HashMap的查詢性能。而使用紅黑樹則能夠改善這種狀況,由於紅黑樹有良好的自平衡性質,不會出現如上圖所示的狀況,而且能將查詢速度控制在log(n)上。

基本操做中看原理

咱們根據put方法去查看HashMap的存儲機制:

其中的這個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)
            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;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)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;
                    }
                    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;
    }
複製代碼

其中咱們能夠很清晰的看到HashMap計算hash值的代碼以下:

他經過這個計算出改key應該處於哈希表中的哪個桶中即哪個索引上。 以後又根據判斷投鏈表中是否已有這個Key或者這個鏈表的頭結點是否是樹類型的節點,來進行後面的操做。判斷的代碼以下:

若是已存在根據onlyIfAbsent來決定是否更新,若是不存在,則判斷是否是樹類型的節點來進行操做,是的話,調用putTreeVal方法,不是的話,則迭代鏈表進行查詢是否已有此key,而且根據binCount來記錄鏈表中元素個數,當binCount超過TREEIFY_THRESHOLD(默認爲8)時將此鏈表進行樹化。

擴容機制

咱們先從構造函數提及:

咱們能夠看到,在構造函數中,咱們並無直接使用initialCapacity,而是使用了tableSizeFor來進行了一次計算並設置了threshold的值即下一次擴容時的閾值。咱們來看一下tableSizeFor這個方法:

看註釋是返回給定目標容量的最接近的2的冪數,這句話翻譯不如直接看結果,咱們把代碼沾出來跑一下:

運行結果以下:

能夠看出這個函數是根據初始容量來說下一個擴容的閾值調整爲2`n。那麼這麼調整有什麼用呢?爲何擴容閾值必須是2的冪呢?這個咱們後面就會講到。

如今咱們正式開始探討HashMap的擴容機制,咱們在上一節中粘了putVal的代碼,而且注意到,當table即HashMap數組的大小爲0或者,增長元素後,size的大小大於threshold的大小時會進行擴容,其實無論設置初始容量爲多大,在進行第一次插入的時候都會進行一次擴容。這個能夠在源碼中體現出來。咱們如今看看肉resize()這個方法:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        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;
                            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;
    }
複製代碼

在這個方法的註釋上,是這麼解釋的,初始化或者將table的size加倍。若是table爲null,則根據threshold中的值進行分配,不然,由於咱們正在使用二次冪擴展,因此每一個bin中的元素必須保持相同的索引,或者在新表中以兩個偏移的冪移動。那麼這句話是啥意思呢,別急,快講到了。

先看看這個resize()幹了點啥,首先resize方法先根據舊的數組的大小計算出新的數組的大小,而後根據新的容量以及負載因子計算出新的閾值。 咱們知道數組不能動態調整大小,因此只能是從新生成一個數組,並對新數組進行操做,resize方法計算完新的容量與新的閾值後就開始了這一步驟。

在進行舊數組對新數組的複製時,因爲容量是double增加,因此,原來在一個bucket中也就是一個索引中的一個鏈表將拆分紅兩個分別分配到新的數組中不一樣的索引也就是不一樣的bucket中。若是這個鏈表的長度爲1也就是隻有一個節點則根據新的容量直接計算新的索引,並將此節點直接放到計算出的索引位置。

若是此鏈表是紅黑樹的話則進行相應的分裂函數,並將一棵樹分裂成兩顆分配到對應的兩個索引位置。

那舊的索引裏的應該分配到哪兩個索引位置呢?其實在註解中都說了,應該分配到舊的索引位置,以及舊的索引位置+舊的容量的索引位置處。以下圖:

擴容後:

能夠發現原來在某個索引index上的節點被分到 index與index+oldCap的2索引上。

那在HashMap中這種分配是怎麼實現的呢?咱們繼續研究:

在這裏的代碼中,咱們發現HashMap經過元素的hash值&oldCap是否等於0來將進行分割。

回到前面的問題,容量爲何是2的n次方呢?當咱們利用一個hash碼去求一個數組的索引,通常都是hash%length,也就是取餘數的方式,而當這個length爲2的n次方時 hash&(length-1) = hash % length,而且按位運算比較快,因此這就是爲何容量是2的n次方的緣由。

總結

  1. HashMap底層存儲結構是數組+鏈表 當鏈表的長度超過8則會進行樹化,轉換成數組+紅黑樹的存儲形式,當鏈表長短少於6時會有紅黑樹轉回鏈表。

  2. HashMap擴容後,容量爲原來的二倍,舊的索引位置上的鏈表會被分紅兩份存放在新數組的這個索引位置以及這個索引加上舊的容量的索引位置處。

  3. HashMap的特色就是高性能,查詢與存取性能都是高效的。缺點是元素無序。不是同步的。

  4. 存入某鍵值對時的順序是根據hashCode得到索引位置,以後調用equals方法進行比較。

相關文章
相關標籤/搜索