HashMap關鍵源碼分析及面試題

BAT面試題

1.HashMap的什麼時候擴容,哪些操作會觸發

        當向容器添加元素的時候,會判斷當前容器的元素個數,如果大於等於閾值,即當前數組的長度乘以加載因子的值的時候,就要自動擴容。默認容量爲16,擴容因子是0.75,閾值爲12。

        有參構造方法和put、merge操作都會導致擴容。

2.HashMap push方法的執行過程? 

        最先判斷桶的長度是否爲0,爲0的話則需要先對桶進行初始化操作,接着,求出hashcode並通過擾動函數確定要put到哪個桶中,若桶中沒有元素直接插入,若有元素則判斷key是否相等,如果相等的話,那麼就將value改爲我們put的value值,若不等的話,那麼此時判斷該點是否爲樹節點,如果是的話,調用putreeval方法,以樹節點的方式插入,如果不是樹節點,那麼就遍歷鏈表,如果找到了key那麼修改value,沒找到新建節點插到鏈表尾部,最後判斷鏈表長度是否大於8 是否要進行樹化。

3.HashMap檢測到hash衝突後,將元素插入在鏈表的末尾還是開頭? 

        因爲JDK1.7是用單鏈表進行的縱向延伸,採用頭插法就是能夠提高插入的效率,但是也會容易出現逆序且環形鏈表死循環問題。但是在JDK1.8之後是因爲加入了紅黑樹使用尾插法,能夠避免出現逆序且鏈表死循環的問題。

4.1.8還採用了紅黑樹,講講紅黑樹的特性,爲什麼人家一定要用紅黑樹而不是AVL、B樹之類的?

        在CurrentHashMap中是加鎖了的,實際上是讀寫鎖,如果寫衝突就會等待,如果插入時間過長必然等待時間更長,而紅黑樹相對AVL樹B樹的插入更快,AVL樹查詢確實更快一些,但是對於操作密集型,紅黑樹的旋轉更少,效率更高。

5.HashMap get方法的執行過程? 

        首先和put一樣,確定對應的key在哪一個桶中,如果桶容量爲0或者該桶內沒有元素直接返回空,反之會判斷該桶會檢查桶中第一個元素是否和要查的key相等,相等的話直接返回,不相等的話判斷該節點是否爲樹節點,是的話以樹節點方式遍歷整棵樹來查找,不是的話那就說明存儲結構是鏈表,以遍歷鏈表的方式查找。

源碼與其中的算法技巧

  1. 構造方法

public HashMap(int initialCapacity, float loadFactor) {


    //當指定的 initialCapacity (初始容量) < 0 的時候會拋出 IllegalArgumentException 異常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);


    //當指定的 initialCapacity (初始容量)= MAXIMUM_CAPACITY(最大容量) 的時候 
    if (initialCapacity > MAXIMUM_CAPACITY)
        //初始容量就等於 MAXIMUM_CAPACITY (最大容量)
        initialCapacity = MAXIMUM_CAPACITY;


    //當 loadFactory(負載因子)< 0 ,或者不是數字的時候會拋出 IllegalArgumentException 異常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;


    //tableSizeFor()的主要功能是返回一個比給定整數大且最接近2的冪次方整數
    //比如我們給定的數是12,那麼tableSizeFor()會返回2的4次方,也就是16,因爲16是最接近12並且大於12的數
    this.threshold = tableSizeFor(
        initialCapacity);
}

        執行順序註釋寫的很清楚了,但是有些同學對最後對 tableSizeFor 方法很有疑問,這是用來求傳入容量的最小2的冪次方整數的。

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

        這是一系列的或操作,舉個例子

n-=1;// n=1000000(二進制)
...//16、8無變化
n|=n>>>4;//n=n|(n>>>4)=1000000|0000100=1000100
n|=n>>>2;//n=n|(n>>>2)=1000100|0010001=1010101
...

        看出規律來了吧,右移多少位,就把最高位右邊的第x位設置爲1;第二次,就把兩個爲1的右邊xx位再設置爲1;第n次,就把上一步出現的1右邊xxxx位置爲1;

這樣執行完,原來是1000000,變成了1111111,最後加1,就變成2的整數次方數了。之所以先減一是因爲有可能本身就是最小2的冪次方整數。

2.Put方法

        put方法的核心就是 putVal ,源碼和執行過程如下。

//實現 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,則進行resize()(擴容)
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;


    //確定插入table的位置,算法是上面提到的 (n - 1) & hash,在 n 爲 2 的時候,相當於取模操作
    if ((p = tab[i = (n - 1) & hash]) == null)
        //找到key值對應的位置並且是第一個,直接插入
        tab[i] = newNode(hash, key, value, null);


    //在table的 i 的位置發生碰撞,分兩種情況
    //1、key值是一樣的,替換value值
    //2、key值不一樣的
    //而key值不一樣的有兩種處理方式:1、存儲在 i 的位置的鏈表 2、存儲在紅黑樹中
    else {
        Node<K,V> e; K k;


        //第一個Node的hash值即爲要加入元素的hash
        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 {
            //如果不是TreeNode的話,即爲鏈表,然後遍歷鏈表
            for (int binCount = 0; ; ++binCount) {


                //鏈表的尾端也沒有找到key值相同的節點,則生成一個新的Node
                //並且判斷鏈表的節點個數是不是到達轉換成紅黑樹的上界達到,則轉換成紅黑樹
                if ((e = p.next) == null) {


                    //創建鏈表節點並插入尾部
                    p.next = newNode(hash, key, value, null);


                    //超過了鏈表的設置長度(默認爲8)則轉換爲紅黑樹
                    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;
            }
        }


        //如果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;
}

        爲了方便理解,配圖

putVal中有一段代碼提到了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;


    //判斷Node的長度,如果不爲零
    if (oldCap > 0) {
        //判斷當前Node的長度,如果當前長度超過 MAXIMUM_CAPACITY(最大容量值)
        if (oldCap >= MAXIMUM_CAPACITY) {
            //新增閥值爲 Integer.MAX_VALUE
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }


        //如果小於這個 MAXIMUM_CAPACITY(最大容量值),並且大於 DEFAULT_INITIAL_CAPACITY (默認16)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            //進行2倍擴容
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        //指定新增閥值
        newCap = oldThr;


    //如果數組爲空
    else {               // zero initial threshold signifies using defaults
        //使用默認的加載因子(0.75)
        newCap = DEFAULT_INITIAL_CAPACITY;
        //新增的閥值也就爲 16 * 0.75 = 12
        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數組
    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;


                        //哈希值和原數組長度進行&操作,爲0則在原數組的索引位置
                        //非0則在原數組索引位置+原數組長度的新位置
                        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;



爲什麼會hash衝突?

        就是根據key即經過一個函數f(key)得到的結果的作爲地址去存放當前的key,value鍵值對(這個是hashmap的存值方式),但是卻發現算出來的地址上已經有數據。這就是所謂的hash衝突。

hash衝突的幾種情況:

        1兩個節點的key值相同(hash值一定相同),導致衝突 

        2 兩個節點的key值不同,由於hash函數的侷限性導致hash值相同,導致衝突 

        3 兩個節點的key值不同,hash值不同,但hash值對數組長度取模後相同,導致衝突 

        

        如何解決hash衝突?解決hash衝突的方法主要有兩種,一種是開放尋址法,另一種是鏈表法 。


開放尋址法--線性探測

        開放尋址法的原理很簡單,就是當一個Key通過hash函數獲得對應的數組下標已被佔用的時候,我們可以尋找下一個空檔位置

        比如有個Entry6通過hash函數得到的下標爲2,但是該下標在數組中已經有了其它的元素,那麼就向後移動1位,看看數組下標爲3的位置是否有空位

        但是下標爲3的數組也已經被佔用了,那麼久再向後移動1位,看看數組下標爲4的位置是否爲空

        數組下標爲4的位置還沒有被佔用,所以可以把Entry6存入到數組下標爲4的位置。這就是開放尋址的基本思路,尋址的方式有很多種,這裏只是簡單的一個示例

鏈表法

        鏈表法也正是被應用在了HashMap中,HashMap中數組的每一個元素不僅是一個Entry對象,還是一個鏈表的頭節點。每一個Entry對象通過next指針指向它的下一個Entry節點。當新來的Entry映射到與之衝突的數組位置時,只需要插入到對應的鏈表中即可

額…… 寫不完了 其他操作下篇繼續 

◆ ◆ ◆  ◆ 

關注並後臺回覆 “面試” 或者  “視頻

即可免費獲取最新2019BAT

大廠面試題和大數據微服務視頻

您的分享和支持是我更新的動力

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章