以最簡單的方式講HashMap


 

以最簡單的方式講HashMap

HashMap可以說是面試中最常出現的名詞,這次頭條的一面,第一個問的問題就是HashMap。所以就讓我們來探討下HashMap吧。

實驗環境:JDK1.8

首先先說一下,和JDK1.7相比,對HashMap做了一些優化,使得HashMap的性能更加的優化。

  1. HashMap的儲存結構

  2. HashMap中的Hash

  3. HashMap是怎么保存數據的

  4. HashMap的擴容操作

  5. HashMap的線程安全問題

HashMap的儲存結構

只有當我們知道HashMap的儲存結構時,我們才能夠明白HashMap的工作原理。

jdk1.7的存儲結構

在JDK1.7中,HashMap采用的是數組【位桶】+單鏈表的數據結構

 


 

 

圖片來自這里

jdk1.8的儲存結構

在JDK1.8中,與JDK1.7最不相同的地方就是,采用了紅黑樹進行儲存,采用的是數組【位桶】+鏈表+紅黑樹,當鏈表的長度超過某一閥值時,就會將鏈表轉換為紅黑樹,這個閥值可以自己設置,默認是8。

 


 

 

圖片來自這里

Hash

首先先說HashMap中的hash。當我們使用HashMap中的put(k,v)時,

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

首先我們要根據key算出key的hash值。

  • JDK1.8
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

這個hash值不僅僅是通過Object中的hashCode的得到的,還需要進行右移和^位異或。

HashMap保存數據

總所周知,HashMap默認的容量大小是16,那么當我們儲存一個值時,是怎么判斷儲存的位置呢?

首先我們需要明白幾個參數。在使用HashMap的時候我們很可能會使用以下的構造參數:

public HashMap(int initialCapacity, float loadFactor) ;
  • initialCapacity:初始化容量默認是16
  • capacity:容量,通過initCapacity計算出一個大於或者等於initCapacity且為2的冪的值
  • loadFactor:裝載因子,默認是0.75,根據它來確定需要擴容的閥值。
  • threshold:閥值,capacity*loadFactor即為閥值。
  1. 未產生hash沖突

    // n是HashMap的大小,Hash為key的hash值,tab為如下圖中的table,i代表儲存的位置
    int i;
    // 為null代表此位置為空的
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    

     


     

    例如:當某一hash值與(n-1)相與的結果是3,那么就將這個這個table的第3號的位置。

     

  2. 產生hash沖突

    但是如果當我們得到的hash值一樣或者說相與的結果的table位置已經存在一個值了,那么我們應該怎么去儲存呢?

    • 當key與table[i]的所有key進行equals比較,如果相同則直接更新覆蓋value。

    • 假如key進行equals比較不相同,則進行元素的插入操作(在jdk1.7中是鏈表的插入,在jdk1.8中既有鏈表的插入操作也有紅黑樹的操作)。

HashMap保存數據的JKD1.8源代碼看源代碼能夠更好的理解HashMap的put操作

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是空的或者說長度為0,則進行擴容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 假如桶中的元素是空的,則直接將元素放在桶中【使用(n - 1) & hash]判斷放的位置】
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        // 假如桶中已經存在這個元素
        else {
            Node<K,V> e; K k;
            // 假如桶中的第一個元素p的hash值,key與要存的值相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;// 使用e來記錄p
            // TreeNode 代表紅黑樹節點
            // 假如key不相等,則將元素放入紅黑樹節點中
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 假如p為鏈表節點
            else {
                // 進行鏈表查找
                for (int binCount = 0; ; ++binCount) {
                    // 假如next為空【代表達到鏈表末尾】
                    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))))
                        // 此時e為鏈表中key相等的元素
                        break;
                    p = e;
                }
            }
            // e不為nul,代表要相同的元素
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // 如果onlyIfAbsent為false或者舊值為空,則進行更新
                // 在源碼中onlyIfAbsent默認是false
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 回調以允許LinkedHashMap事后操作
                afterNodeAccess(e);
                // 返回舊值
                return oldValue;
            }
        }
        // modeCount代表HashMap在結構上面被修改的次數
        ++modCount;
        // 加入大小大於閥值則進行擴容
        if (++size > threshold)
            resize();
        // 回調以允許LinkedHashMap事后操作
        afterNodeInsertion(evict);
        return null;
    }

HashMap的擴容操作

在HashMap中進行擴容操作是特別耗費時間的,因為隨着擴容,會重新進行一次hash分配,遍歷hash表中的所有元素,因為桶的大小【也就是數組長度n】變了,那么(n - 1) & hash的值也會發生改變,所以我們在編寫程序時應該盡量避免resize,盡量在新建HashMap對象的時候指令桶的長度【阿里巴巴開發手冊也是這樣推薦使用】。

HashMap進行擴容時,會完全新建一個桶,我們從上面了解到桶就是數組,而數組是沒辦法自動擴容的,所以我們需要用一個新的數組來代替前面的桶。而當HashMap進行擴容是,閥值會變成原來的兩倍容量也會變成原來的兩倍

首先我們先講講JDK1.7中的resize(),JDK1.8有紅黑樹,還是有點麻煩。

  1. JDK1.7 的rezise()
void resize(int newCapacity) {   //傳入新的容量 
    //table為擴容前的Entry數組
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;  
    // 如果擴容前的數組大小如果已經達到最大(2^30) 
    if (oldCapacity == MAXIMUM_CAPACITY) {  
        //修改閾值為int的最大值(2^31-1),這樣以后就不會擴容了 
        threshold = Integer.MAX_VALUE;
        return;  
    }  

    // 新建一個Entry數組
    Entry[] newTable = new Entry[newCapacity];  
    //將數據轉移到新的Entry數組里
    transfer(newTable);
    // 修改table的指向對象
    table = newTable;
    threshold = (int) (newCapacity * loadFactor);//修改閾值 
}

void transfer(Entry[] newTable) {  
    Entry[] src = table;                   //src引用了舊的Entry數組 
    int newCapacity = newTable.length;  
    // 遍歷舊的Entry數組 
    for (int j = 0; j < src.length; j++) { 
        Entry<K, V> e = src[j];
        // 如果此位置存在元素
        if (e != null) {  
            // for循環過后,舊的Entry數組就不再引用任何對象
            src[j] = null;
            // 遍歷鏈表
            do {  
                // 獲得鏈表中的下一個元素
                Entry<K, V> next = e.next;  
                // 重新計算數據保存位置
                int i = indexFor(e.hash, newCapacity);
                // 在jdk1.7中是頭部插入,此時e.next指向新的數組位置newTable[i]
                e.next = newTable[i];
                // 將newTable指向e
                newTable[i] = e;
                // 訪問下一個Entry鏈上的元素
                e = next;
            } while (e != null);  
        }  
    }  
}  
static int indexFor(int h, int length) {  
    return h & (length - 1);  
}  
  1. JDK1.8 的rezise()

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    // 獲得table的大小,並將其長度賦值給oldCap
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 閥值賦值
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 如果table不為空
    if (oldCap > 0) {
        // 數組大小大於(2^30)
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 修改閾值為int的最大值(2^31-1),這樣以后就不會擴容了 
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // newCap = oldCap << 1新的容量為以前的兩倍
        // 當新的table長度沒有超過最導致,且以前的table長度大於16,則進行閥值更新
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 閥值擴大成兩倍
            newThr = oldThr << 1; // double threshold
    }
    // 如果table為空,且閥值大於0
    else if (oldThr > 0) // initial capacity was placed in threshold
        // 則新的容量大小為閥值
        newCap = oldThr;
    
    // 假如table為空切閥值小於等於0,則初始化閥值,和table
    else {               // zero initial threshold signifies using defaults
        // 新的table長度為16
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 新的閥值為負載因子【0.75】*16
        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;
    /* *以上都是進行初始化操作,目的是擴大容量,或則初始化HashMap *下面便是重新存放元素操作 */

    @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;
            // 假如oldTab[j]中含有元素
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 假如沒有下一個元素,也就是oldTab[j]中只有e一個元素
                if (e.next == null)
                    // 重新選擇空間
                    newTab[e.hash & (newCap - 1)] = e;
                // 假如有下一個元素,且該節點為紅黑樹節點
                else if (e instanceof TreeNode)
                    // 將該節點進行rehash后,放到新的地方
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                /** * 在JDK1.8中不像JDK1.7一樣重新進行hash值計算,而是利用了一個規律: * 假如e.hash & oldCap為0,那么該元素的引索位置沒有變 * 假如e.hash & oldCap為1,那么該元素的引索位置為原引索+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;
}

HashMap的線程安全問題

相信很多人都聽說過HashMap線程不安全,但是HashMap為什么會產生線程安全問題呢?

  1. 多線程put()操作

設想一個場景,A線程正在進行put操作,它經過hash計算,以及鏈表查找,已經確定了put的位置X,但是這時候cpu時間片到了,A線程不得不退出put操作的執行,這時候B線程獲得了cpu時間片,在X的位置進行插入值,如果A線程再執行put操作就會覆蓋以前的值,此時數據就不一致了。

  1. 多線程resize()操作

當多個線程進行resize()操作時,假如table已經變成新數組,那么下一個線程會使用已經被賦值過得的table做為初始值進行操作。這樣可能就會出現死循環的操作。

至於怎么避免HashMap的多線程安全問題,ConcurrentHashMap是一個好東西,至於它是怎么解決並發的問題,我們下次再聊。

HashMap其實並不是很難,我們主要是要理解它儲存元素的思想與方法。而通過源代碼,我們能夠更好的理解設計的理念


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM