一篇文章徹底讀懂HashMap之HashMap源碼解析

在秋招面試準(zhǔn)備中博主找過很多關(guān)于HashMap的博客滚澜,但是秋招結(jié)束后回過頭來看粗仓,感覺沒有一篇全面、通俗易懂的講解HashMap文章(可能是博主沒有找到)设捐,所以在秋招結(jié)束后借浊,寫下了這篇文章,盡最大的努力把HashMap源碼講解的通俗易懂萝招,并且盡量涵蓋面試中HashMap的考察點(diǎn)蚂斤。

就博主的經(jīng)歷來看,HashMap是求職面試中名副其實(shí)的“明星”槐沼,基本上博主面試的每一家公司多多少少都有問到HashMap的底層實(shí)現(xiàn)原理等一些相關(guān)問題曙蒸。

這篇文章將會按以下順序來組織:

HashMap源碼分析(JDK8,通俗易懂)
HashMap面試“明星”問題匯總岗钩,以及明星問題答案

下面是JDK8中HashMap的源碼分析:

注:下文源碼分析中纽窟,

注釋多少與重要性成正比

注釋多少與重要性成正比

注釋多少與重要性成正比

HashMap的成員屬性源碼分析

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    private static final long serialVersionUID = 362498820763181265L;
    /**
     * The default initial capacity - MUST be a power of two.
     */

    //HashMap的初始容量為16,HashMap的容量指的是存儲元素的數(shù)組大小兼吓,即桶的數(shù)量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    
    //HashMap的最大的容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */

   /**
     * DEFAULT_LOAD_FACTOR:HashMap的負(fù)載因子臂港,影響HashMap性能的參數(shù)之一,是時間和空間之間的權(quán)
     * 衡视搏,后面會看到HashMap的元素存儲在Node數(shù)組中审孽,這個數(shù)組的大小這里稱為“桶”的大小.
     * 另外還有一個參數(shù)size指的是我們往HashMap中put了多少個元素。
     * 當(dāng)size==桶的數(shù)量*DEFAULT_LOAD_FACTOR的時候浑娜,這時HashMap要進(jìn)行擴(kuò)容操作佑力,也就是桶不能裝
     * 滿。DEFAULT_LOAD_FACTOR是衡量桶的利用率:DEFAULT_LOAD_FACTOR較小時(桶的利用率較薪钤狻),
     * 這時浪費(fèi)的空間較多(因?yàn)橹荒艽鎯ν暗臄?shù)量*DEFAULT_LOAD_FACTOR個元素打颤,超過了就要進(jìn)行擴(kuò)容),
     * 這種情況下往HashMap中put元素時發(fā)生沖突的概率也很小杂数,所謂沖突指的是:多個元素被put到了同一個桶 中;
     * 沖突小時(可以認(rèn)為一個桶中只有一個元素)put、get等HashMap的操作代價(jià)就很低瘸洛,可以認(rèn)為是O(1);
     * 桶的利用率較大的時候(DEFAULT_LOAD_FACTOR很大揍移,注意可以大于1,因?yàn)闆_突的元素是使用鏈表或者 紅黑樹連接起來的)
     * 空間利用率較高反肋,
     * 這也意味著一個桶中存儲了很多元素那伐,這時HashMap的put、get等操作代價(jià)就相對較大石蔗,因?yàn)槊恳粋€put或get操作都變成了
     * 對鏈表或者紅黑樹的操作罕邀,代價(jià)肯定大于O(1),所以說DEFAULT_LOAD_FACTOR是空間和時間的一個平衡點(diǎn);
     * DEFAULT_LOAD_FACTOR較小時,需要的空間較大养距,但是put和get的代價(jià)較小;
     * DEFAULT_LOAD_FACTOR較大時诉探,需要的空間較小,但是put和get的代價(jià)較大)棍厌。
     * 
     * 擴(kuò)容操作就是把桶的數(shù)量*2肾胯,即把Node數(shù)組的大小調(diào)整為擴(kuò)容前的2倍,至于為什么是兩倍耘纱,分析擴(kuò)容函 數(shù)時會講解敬肚,
     * 這其實(shí)是一個trick。Node數(shù)組中每一個桶中存儲的是Node鏈表束析,當(dāng)鏈表長度>=8的時候艳馒,
     * 鏈表會變?yōu)榧t黑樹結(jié)構(gòu)(因?yàn)榧t黑樹的增刪改查復(fù)雜度是logn,鏈表是n员寇,紅黑樹結(jié)構(gòu)比鏈表代價(jià)更信俊)。
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    //當(dāng)某一個桶中鏈表的長度>=8時蝶锋,鏈表結(jié)構(gòu)會轉(zhuǎn)換成紅黑樹結(jié)構(gòu)(其實(shí)還要求桶的中數(shù)量>=64,后面會提到)
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    //當(dāng)紅黑樹中的節(jié)點(diǎn)數(shù)量<=6時陆爽,紅黑樹結(jié)構(gòu)會轉(zhuǎn)變?yōu)殒湵斫Y(jié)構(gòu)
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    //上面提到的:當(dāng)Node數(shù)組容量>=64的前提下,如果某一個桶中鏈表長度>=8牲览,則會將鏈表結(jié)構(gòu)轉(zhuǎn)換成  
    //紅黑樹結(jié)構(gòu)
    static final int MIN_TREEIFY_CAPACITY = 64;

}

HashMap內(nèi)部類——Node源碼分析
下面介紹HashMap的內(nèi)部類Node墓陈,HashMap的所有數(shù)據(jù)都保存在Node數(shù)組中那么這個Node到底是個什么東西呢恶守?

//Node是HashMap的內(nèi)部類
static class Node<K,V> implements Map.Entry<K,V> {
        //保存key的hashcode=key的hashcode ^ (key的hashcode>>>16)這樣做主要是為了減少hash沖突
       //當(dāng)我們往map中put(k,v)時第献,這個k,v鍵值對會被封裝為Node,那么這個Node放在Node數(shù)組的哪個
      //位置呢:index=hash&(n-1),n為Node數(shù)組的長度兔港。那為什么這樣計(jì)算hash可以減少沖突呢庸毫?如果直接
      //使用hashCode&(n-1)來計(jì)算index,此時hashCode的高位隨機(jī)特性完全沒有用到衫樊,因?yàn)閚相對于 
      //hashcode的值很小飒赃±ǎ基于這一點(diǎn),把hashcode 高16位的值通過異或混合到hashCode的低16位载佳,由此 
      //來增強(qiáng)hashCode低16位的隨機(jī)性
        final int hash; 
        final K key;//保存map中的key
        V value;//保存map中的value
        Node<K,V> next;//單向鏈表

        Node(int hash, K key, V value, Node<K,V> next) {//構(gòu)造器
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

HashMap hash函數(shù)和tableSizeFor函數(shù)源碼分析(這兩個函數(shù)后面會用到)

    /**
     * HashMap允許key為null炒事,null的hash為0(也意味著HashMap允許key為null的鍵值對),
     * 非null的key的hash高16位和低16位分別由由:key的hashCode
     * 高16位和hashCode的高16位異或hashCode的低16位組成蔫慧。主要是為了增強(qiáng)hash的隨機(jī)性減少hash&(n-1)的
     * 隨機(jī)性挠乳,即減小hash沖突,提高HashMap的性能姑躲。所以作為HashMap的key的hashCode函數(shù)的實(shí)現(xiàn)對HashMap
     * 的性能影響較大睡扬,極端情況下:所有key的hashCode都相同,這是HashMap的性能很糟糕黍析!
     */
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }


    /**
     * 在new HashMap的時候卖怜,如果我們傳入了大小參數(shù),這是HashMap會對我們傳入的HashMap容量進(jìn)行傳到
     * tableSizeFor函數(shù)處理:這個函數(shù)主要功能是:返回一個數(shù):這個數(shù)是大于等于cap并且是2的整數(shù)次冪
     * 的所有數(shù)中最小的那個阐枣,即返回一個最接近c(diǎn)ap(>=cap)马靠,并且是2的整數(shù)次冪的那個數(shù)。
     * 具體邏輯如下:一個數(shù)是2的整數(shù)次冪蔼两,那么這個數(shù)減1的二進(jìn)制就是一串掩碼虑粥,即二進(jìn)制從某位開始是一 串連續(xù)的1。
     */
    static final int tableSizeFor(int cap) {
        //舉例而言:n的第三位是1(從高位開始數(shù))宪哩, 
        int n = cap - 1;

        n |= n >>> 1; 
        n |= n >>> 2; 
        n |= n >>> 4; 
        n |= n >>> 8; 
        n |= n >>> 16; 
        //舉例而言:如果n為: 00010000 00000000 00000000 000
        /*
        n |= n >>> 1;->n:00011000 00000000 00000000 0000
        n |= n >>> 2;->n: 00011110 00000000 00000000 0000
        n |= n >>> 4;->n: 00011111 11100000 00000000 0000
        n |= n >>> 8;->n: 00011111 11111111 11100000 0000
        n |= n >>> 16;->n:00011111 11111111 11111111 1111
        
        返回n+1:00010000 00000000 00000000 000(>=cap并且為2的整數(shù)次冪娩贷,與cap差值最小的那個)
        最后的n+1一定是2的整數(shù)次冪,并且一定是>=cap
        整體的思路就是:如果n二進(jìn)制的第k為1锁孟,那么經(jīng)過上面四個‘|’運(yùn)算后[0,k]位都變成了1,
        即:一連串連續(xù)的二進(jìn)制‘1’(掩碼)彬祖,最后n+1一定是2的整數(shù)次冪(如果不溢出)
        */
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

HashMap成員屬性源碼分析

/**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    //我們往map中put的(k,v)都被封裝在Node中,所有的Node都存放在table數(shù)組中
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    //用于返回keySet和values
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * The number of key-value mappings contained in this map.
     */
    //保存map當(dāng)前有多少個元素
    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).
     */
    //failFast機(jī)制品抽,在講解ArrayList和LinkedList一文中已經(jīng)講過了
    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.)

   //這也是比較重要的一個屬性:
   //創(chuàng)建HashMap時储笑,改變量的值是:初始容量(2的整數(shù)次冪)
   //之后threshold的值是HashMap擴(kuò)容的門限值:即當(dāng)前Node/table數(shù)組的長度*loadfactor

 //舉個例子而言,如果我們傳給HashMap構(gòu)造器的容量大小為9圆恤,那么threshold初始值為16突倍,在向HashMap中
   //put第一個元素后,內(nèi)部會創(chuàng)建長度為16的Node數(shù)組盆昙,并且threshold的值更新為16*0.75=12羽历。
   //具體而言,當(dāng)我們一直往HashMap put元素的時候淡喜,秕磷,如果put某個元素后,Node數(shù)組中元素個數(shù)為13了 
   //炼团,此時會觸發(fā)擴(kuò)容(因?yàn)閿?shù)組中元素個數(shù)>threshold了澎嚣,即13>threshold=12)疏尿,具體擴(kuò)容操作之后會 
  //詳細(xì)分析,簡單理解就是易桃,擴(kuò)容操作將Node數(shù)組長度*2褥琐;并且將原來的所有元素遷移到新的Node數(shù)組中。
    int threshold;

    /**
     * The load factor for the hash table.
     *
     * @serial
     */
    //負(fù)載因子晤郑,見上面對DEFAULT_LOAD_FACTOR 參數(shù)的講解踩衩,默認(rèn)值是0.75
    final float loadFactor;

HashMap構(gòu)造器源碼分析:

//構(gòu)造器:指定map的大小,和loadfactor
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;//保存loadfactor
        
        //注意贩汉,前面有講tableSizeFor函數(shù)驱富,該函數(shù)返回值:>=initialCapacity、返回值是2的整數(shù)次冪
        //并且得是滿足上面兩個條件的所有數(shù)值中最小的那個數(shù)
        this.threshold = tableSizeFor(initialCapacity);
    }
    
    //只指定HashMap容量的構(gòu)造器匹舞,loadfactor使用的是默認(rèn)的值:0.75
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
     //無參構(gòu)造器褐鸥,默認(rèn)loadfactor:0.75
     public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

//其他不常用的構(gòu)造器就不分析了

從構(gòu)造器中我們可以看到:HashMap是“懶加載”,在構(gòu)造器中值保留了相關(guān)保留的值赐稽,并沒有初始化table<Node>數(shù)組叫榕,當(dāng)我們向map中put第一個元素的時候,map才會進(jìn)行初始化姊舵!

HashMap的get函數(shù)源碼分析

//入口,返回對應(yīng)的value
public V get(Object key) {
        Node<K,V> e;
        
        //hash:這個函數(shù)上面分析過了晰绎。返回key混淆后的hashCode
        //注意getNode返回的類型是Node:當(dāng)返回值為null時表示map中沒有對應(yīng)的key,注意區(qū)分value為
        //null:如果key對應(yīng)的value為null的話括丁,體現(xiàn)在getNode的返回值e.value為null荞下,此時返回值也是
        //null,也就是HashMap的get函數(shù)不能判斷map中是否有對應(yīng)的key:get返回值為null時史飞,可能不包 
        //含該key尖昏,也可能該key的value為null!那么如何判斷map中是否包含某個key呢构资?見下面contains            
        //函數(shù)分析
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    public boolean containsKey(Object key) {
        //注意與get函數(shù)區(qū)分抽诉,我們往map中put的所有的<key,value>都被封裝在Node中,如果Node都不存在
        //顯然一定不包含對應(yīng)的key
        return getNode(hash(key), key) != null;
    }



  //下面分析getNode函數(shù)
  /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key //下面簡稱:key的hash值
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //(n-1)&hash:當(dāng)前key可能在的桶索引吐绵,put操作時也是將Node存放在index=(n-1)&hash位置
        //主要邏輯:如果table[index]處節(jié)點(diǎn)的key就是要找的key則直接返回該節(jié)點(diǎn)迹淌;
        //否則:如果在table[index]位置進(jìn)行搜索,搜索是否存在目標(biāo)key的Node:這里的搜索又分兩種:
        //鏈表搜索和紅黑樹搜索己单,具體紅黑樹的查找就不展開了唉窃,紅黑樹是一種弱平衡(相對于AVL)BST,
        //紅黑樹查找荷鼠、刪除句携、插入等操作都能夠保證在O(lon(n))時間復(fù)雜度內(nèi)完成,紅黑樹原理不在本文
        //范圍內(nèi)允乐,但是大家要知道紅黑樹的各種操作是可以實(shí)現(xiàn)的矮嫉,簡單點(diǎn)可以把紅黑樹理解為BST,
        //BST的查找牍疏、插入蠢笋、刪除等操作的實(shí)現(xiàn)在之前的博客中有java實(shí)現(xiàn)代碼,實(shí)際上紅黑樹就是一種平            
        //衡的BST
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;//一次就匹配到了鳞陨,直接返回昨寞,否則進(jìn)行搜索
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    //紅黑樹搜索/查找
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    //鏈表搜索(查找)
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;//找到了就返回
                } while ((e = e.next) != null);
            }
        }
        return null;//沒找到,返回null
    }

get函數(shù)實(shí)質(zhì)就是進(jìn)行鏈表或者紅黑樹遍歷搜索指定key的節(jié)點(diǎn)的過程厦滤;另外需要注意到HashMap的get函數(shù)的返回值不能判斷一個key是否包含在map中援岩,get返回null有可能是不包含該key;也有可能該key對應(yīng)的value為null掏导。HashMap中允許key為null享怀,允許value為null。

HashMap的put函數(shù)源碼分析

//put函數(shù)入口,兩個參數(shù):key和value
public V put(K key, V value) {
        //下面分析這個函數(shù)趟咆,注意前3個參數(shù)添瓷,后面2個參數(shù)這里不太重要,因?yàn)樗械膒ut后面2個參數(shù)都一樣
        return putVal(hash(key), key, value, false, true);
    }



//下面是put函數(shù)的核心處理函數(shù)
/**
     * 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
     */

    //hash:key的hashCode
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //上面提到過HashMap是懶加載值纱,所有put的時候要先檢查table數(shù)組是否已經(jīng)初始化了鳞贷,
        //沒有初始化得先初始化table數(shù)組,保證table數(shù)組一定初始化了
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//這個函數(shù)后面有resize函數(shù)分析
    
        //到這里表示table數(shù)組一定初始化了
        //與上面get函數(shù)相同虐唠,指定key的Node搀愧,put在table數(shù)組的i=(n-1)&hash下標(biāo)位置,get的時候
        //也是從table數(shù)組的該位置搜索
        if ((p = tab[i = (n - 1) & hash]) == null)
            //如果i位置還沒有存儲元素疆偿,則把當(dāng)前的key妈橄,value封裝為Node,存儲在table[i]位置
            tab[i] = newNode(hash, key, value, null);
        else {
       //如果table[i]位置已經(jīng)有元素了翁脆,則接下來執(zhí)行的是:
       //首先判斷鏈表或者二叉樹中時候已經(jīng)存在key的鍵值對眷蚓,存在的話就更新它的value
       //不存在的話把當(dāng)前的key,value插入到鏈表的末尾或者插入到紅黑樹中
       //如果鏈表或者紅黑樹中已經(jīng)存在Node.key等于key反番,則e指向該Node沙热,即
       //e指向一個Node:該Node的key屬性與put時傳入的key參數(shù)相等的那個Node,后面會更新e.value
            Node<K,V> e; K k;

       //為什么get和put先判斷p.hash==hash,下面的if條件中去掉hash的比較也可以邏輯也正確罢缸?
       //因?yàn)閔ash的比較是兩個整數(shù)的比較篙贸,比較的代價(jià)相對較小,key是泛型枫疆,對象的比較比整數(shù)比較
        //代價(jià)大爵川,所以先比較hash,hash相等在比較key
            if (p.hash == hash &&//
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;//e指向一個Node:該Node的key屬性與put時傳入的key參數(shù)相等的那個Node
            else if (p instanceof TreeNode)
               //紅黑樹的插入操作息楔,如果已經(jīng)存在該key的TreeNode寝贡,則返回該TreeNode扒披,否則返回null
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //table[i]處存放的是鏈表,接下來和TreeNode類似
                //在遍歷鏈表過程中先判斷是key先前是否存在圃泡,如果存在則e指向該Node
                //否則將該Node插入到鏈表末尾碟案,插入后判斷鏈表長度是否>=8,是的話要進(jìn)行額外操作
                
                //binCountt最后的值是鏈表的長度
                for (int binCount = 0; ; ++binCount) {
                    
                    if ((e = p.next) == null) {
                   //遍歷到了鏈表最后一個元素,接下來執(zhí)行鏈表的插入操作颇蜡,先封裝為Node再插入
                   //p指向的是鏈表最后一個節(jié)點(diǎn)价说,將待插入的Node置為p.next,就完成了單鏈表的插入
                        p.next = newNode(hash, key, value, null);

                       
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //TREEIFY_THRESHOLD值是8风秤, binCount>=7,然后又插入了一個新節(jié)點(diǎn)鳖目,            
                            //鏈表長度>=8,這時要么進(jìn)行擴(kuò)容操作缤弦,要么把鏈表結(jié)構(gòu)轉(zhuǎn)為紅黑樹結(jié)構(gòu)
                            //我們接下會分析treeifyBin的源碼實(shí)現(xiàn)
                            treeifyBin(tab, hash);
                        break;
                    }
                    
                    //當(dāng)p不是指向鏈表末尾的時候:先判斷p.key是否等于key领迈,等于的話表示當(dāng)前key
                    //已經(jīng)存在了,令e指向p甸鸟,停止遍歷惦费,最后會更新e的value;
                    //不等的話準(zhǔn)備下次遍歷抢韭,令p=p.next薪贫,即p=e
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

            
            if (e != null) { // existing mapping for key
                //表示當(dāng)前的key之前已經(jīng)存在了,并且上面的邏輯保證:e.key一定等于key
                //這是更新e.value就好
                V oldValue = e.value;//保存oldvalue
                
                //onlyIfAbsent默是false刻恭,evict為true
                //onlyIfAbsent為true表示如果之前已經(jīng)存在key這個鍵值對了瞧省,那么后面在put這個key 
                //時,忽略這個操作鳍贾,不更新先前的value鞍匾,這里連接就好 
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//更新e.value
                
                //這個函數(shù)的默認(rèn)實(shí)現(xiàn)是“空”,即這個函數(shù)默認(rèn)什么操作都不執(zhí)行骑科,那為什么要有它呢橡淑?
                //這是個hook/鉤子函數(shù),主要要在LinkedHashMap中咆爽,LinkedHashMap重寫了這個函數(shù)
                //后面講解LinkedHashMap時會詳細(xì)講解
                afterNodeAccess(e);
                return oldValue;//返回舊的value
            }
        }

        //如果是第一次插入key這個鍵梁棠,就會執(zhí)行到這里
        ++modCount;//failFast機(jī)制
        
        //size保存的是當(dāng)前HashMap中保存了多少個鍵值對,HashMap的size方法就是直接返回size
        //之前說過斗埂,threshold保存的是當(dāng)前table數(shù)組長度*loadfactor符糊,如果table數(shù)組中存儲的
        //Node數(shù)量大于threshold,這時候會進(jìn)行擴(kuò)容呛凶,即將table數(shù)組的容量翻倍男娄。后面會詳細(xì)講解
        //resize方法
        if (++size > threshold)
            resize();
        
        //這也是一個hook函數(shù),作用和afterNodeAccess一樣
        afterNodeInsertion(evict);
        return null;
    }

    
    //將鏈表轉(zhuǎn)換為紅黑樹結(jié)構(gòu),在鏈表的插入操作后調(diào)用
/**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        
        //MIN_TREEIFY_CAPACITY值是64,也就是當(dāng)鏈表長度>8的時候模闲,有兩種情況:
        //如果table數(shù)組的長度<64,此時進(jìn)行擴(kuò)容操作
        //如果table數(shù)組的長度>64建瘫,此時進(jìn)行鏈表轉(zhuǎn)紅黑樹結(jié)構(gòu)的操作
        //具體轉(zhuǎn)細(xì)節(jié)在面試中幾乎沒有問的,這里不細(xì)講了围橡,
        //大部同學(xué)認(rèn)為鏈表長度>8一定會轉(zhuǎn)換成紅黑樹暖混,這是不對的B乒薄N淌凇!
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

HashMap的resize函數(shù)源碼分析
重點(diǎn)中的重點(diǎn)晾咪,面試談到HashMap必考resize相關(guān)知識

/**
     *    有兩種情況會調(diào)用當(dāng)前函數(shù): 
     *    1.之前說過HashMap是懶加載收擦,第一次hHashMap的put方法的時候table還沒初始化,
     * 這個時候會執(zhí)行resize谍倦,進(jìn)行table數(shù)組的初始化塞赂,table數(shù)組的初始容量保存在threshold中(如果從構(gòu)造器
     * 中傳入的一個初始容量的話),如果創(chuàng)建HashMap的時候沒有指定容量昼蛀,那么table數(shù)組的初始容
     * 量是默認(rèn)值:16宴猾。即,初始化table數(shù)組的時候會執(zhí)行resize函數(shù)
     *    2.擴(kuò)容的時候會執(zhí)行resize函數(shù)叼旋,當(dāng)size的值>threshold的時候會觸發(fā)擴(kuò)容仇哆,即執(zhí)行resize方法,
     * 這時table數(shù)組的大小會翻倍夫植。
     * 
     *   注意我們每次擴(kuò)容之后容量都是翻倍(*2)讹剔,所以HashMap的容量一定是2的整數(shù)次冪,那么HashMap的
     * 容量為什么一定得是2的整數(shù)次冪呢详民?(面試重點(diǎn)) 要知道原因延欠,首先回顧我們put
     * key的時候,每一個key會對應(yīng)到一個桶里面沈跨,桶的索引是這樣計(jì)算的: index = hash & (n-1)由捎,
     * index的計(jì)算最為直觀的想法是:hash%n,即通過取余的方式把當(dāng)前的key饿凛、value鍵值對散列到各個桶中狞玛;
     * 那么這里為什么不用取余(%)的方式呢?原因是CPU對位運(yùn)算支持較好笤喳,即位運(yùn)算速度很快为居。
     *   另外,當(dāng)n是2的整數(shù)次冪時:hash&(n-1)與hash%(n-1)是等價(jià)的,但是兩者效率來講是不同的杀狡,位運(yùn)算的效率
     * 遠(yuǎn)高于%運(yùn)算蒙畴。基于這一點(diǎn),HashMap中使用的是hash&(n-1)膳凝。這還帶來了一個好處碑隆,就是將舊數(shù)組中的Node遷移到擴(kuò)容后
     * 的新數(shù)組中的時候有一個很方便的特性:HashMap使用table數(shù)組保存Node節(jié)點(diǎn),所以table數(shù)組擴(kuò)容的時候(數(shù)組擴(kuò)容)一
     * 定得是先重新開辟一個數(shù)組蹬音,然后把就數(shù)組中的元素重新散列到新數(shù)組中去上煤。這里舉一個例子來來說明這個特性:下面以Hash初始容量
     * n=16,默認(rèn)loadfactor=0.75舉例(其他2的整數(shù)次冪的容量也是類似的):默認(rèn)容量:n=16著淆,二進(jìn)制:10000劫狠;n-1:15,
     * n-1二進(jìn)制:01111,即一連串1永部。某個時刻独泞,map中元素大于16*0.75=12,即size>12苔埋,此時我們新建了一個數(shù)組懦砂,
     * 容量為擴(kuò)容前的兩倍,newtab组橄,len=32;接下來我們需要把table中的Node搬移(rehash)到newtab荞膘。從table的i=0
     * 位置開始處理,假設(shè)我們當(dāng)前要處理table數(shù)組i索引位置的node,那這個node應(yīng)該放在newtab的那個位置呢玉工?下面的hash表
     * 示node.key對應(yīng)的hash值羽资,也就等于node.hash屬性值,另外為了簡單,下面的hash只寫出了8位(省略的高位的0)瓮栗,實(shí)際上
     * hash是32位:node在newtab中的索引:index=hash%len=hash&(len-1)=hash&(32-1)=hash&31
     * =hash&(0x0001_1111)削罩;再看node在table數(shù)組中的索引計(jì)算:i=hash&(16-1)=hash&15
     * =hash&(0x0000_1111)。注意觀察兩者的異同:i=hash&(0x0000_1111);index=hash&(0x0001_1111)
     * 這個表達(dá)式有個特點(diǎn):index=hash&(0x0001_1111)=hash&(0x0000_1111) |
     * hash&(0x0001_0000) =hash&(0x0000_1111) | hash&n)=i+( hash&n)费奸。什么意思呢:
     * hash&n要么等于n要么等于0;也就是:inde要么等于i弥激,要么等于i+n;再具體一點(diǎn):當(dāng)hash&n==0的時候,index=i;
     * 當(dāng)hash&n==n的時候愿阐,index=i+n;這有什么用呢微服?當(dāng)我們把table[i]位置的所有Node遷移到newtab中去的時候:
     * 這里面的node要么在newtab的i位置(不變),要么在newtab的i+n位置缨历;也就是我們可以這樣處理:把table[i]這個桶中的
     * node拆分為兩個鏈表l1和類:如果hash&n==0以蕴,那么當(dāng)前這個node被連接到l1鏈表;否則連接到l2鏈表辛孵。這樣下來丛肮,
     * 當(dāng)遍歷完table[i]處的所有node的時候,我們得到兩個鏈表l1和l2魄缚,這時我們令newtab[i]=l1,newtab[i+n]=l2,
     * 這就完成了table[i]位置所有node的遷移/rehash宝与,這也是HashMap中容量一定的是2的整數(shù)次冪帶來的方便之處焚廊。
     * 下面的resize的邏輯就是上面講的那樣。將table[i]處的Node拆分為兩個鏈表习劫,這兩個鏈表再放到newtab[i]
     * 和newtab[i+n]位置
    
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//保留擴(kuò)容前數(shù)組引用
        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;
            }
            //正常擴(kuò)容:newCap = oldCap << 1
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //容量翻倍咆瘟,擴(kuò)容后的threshold自然也是*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
            //table數(shù)組初始化的時候會進(jìn)入到這里
            newCap = DEFAULT_INITIAL_CAPACITY;//默認(rèn)容量
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//threshold
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;//更新threshold
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//擴(kuò)容后的新數(shù)組
        table = newTab;//執(zhí)行容量翻倍的新數(shù)組
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {//之后完成oldTab中Node遷移到table中去
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)//j這個桶位置只有一個元素,直接rehash到table數(shù)組
                        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;//第一個鏈表l1
                        Node<K,V> hiHead = null, hiTail = null;//第二個鏈表l2
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {//rehash到table[j]位置
                            //將當(dāng)前node連接到l1上
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                 //將當(dāng)前node連接到l2上
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
    
                        if (loTail != null) {
                            //l1放到table[j]位置
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            //l1放到table[j+oldCap]位置
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

HashMap面試“明星”問題匯總,及答案 :

你知道HashMap嗎谤狡,請你講講HashMap灸眼?

這個問題不單單考察你對HashMap的掌握程度,也考察你的表達(dá)豌汇、組織問題的能力幢炸。個人認(rèn)為應(yīng)該從以下幾個角度入手(所有常見HashMap的考點(diǎn)問題總結(jié)):

size必須是2的整數(shù)次方原因
get和put方法流程
resize方法
影響HashMap的性能因素(key的hashCode函數(shù)實(shí)現(xiàn)泄隔、loadFactor拒贱、初始容量)
HashMap key的hash值計(jì)算方法以及原因(見上面hash函數(shù)的分析)
HashMap內(nèi)部存儲結(jié)構(gòu):Node數(shù)組+鏈表或紅黑樹
table[i]位置的鏈表什么時候會轉(zhuǎn)變成紅黑樹(上面源碼中有講)
HashMap主要成員屬性:threshold、loadFactor佛嬉、HashMap的懶加載
HashMap的get方法能否判斷某個元素是否在map中
HashMap線程安全嗎逻澳,哪些環(huán)節(jié)最有可能出問題,為什么暖呕?
HashMap的value允許為null斜做,但是HashTable和ConcurrentHashMap的valued都不允許為null,試分析原因湾揽?
HashMap中的hook函數(shù)(后面講解LinkedHashMap時會講解瓤逼,這可作為HashMap的延伸知識,增加面試官對你的印象)

上面問題的答案都可以在上面的源碼分析中找到库物,下面在給三點(diǎn)補(bǔ)充:

HashMap的初試容量設(shè)計(jì)怎樣影響HashMap的性能的霸旗?
假如你估計(jì)你最多往HashMap中存儲64個元素,那么你在創(chuàng)建HashMap的時候:如果選用無參構(gòu)造器:默認(rèn)容量16戚揭,在存儲16loadFactor個元素之后就要進(jìn)行擴(kuò)容(數(shù)組擴(kuò)容涉及到連續(xù)空間的分配诱告,Node節(jié)點(diǎn)的rehash,代價(jià)很高民晒,所以要盡量避免擴(kuò)容操作)精居;如果給構(gòu)造器傳入的參數(shù)是64,這時HashMap中在存儲64loadFactor個元素之后就要進(jìn)行擴(kuò)容潜必;但是如果你給構(gòu)造器傳的參數(shù)為:(int)(64/0.75)+1靴姿,此時就可以保證HashMap不用進(jìn)行擴(kuò)容,避免了擴(kuò)容時的代價(jià)磁滚。

HashMap線程安全嗎佛吓,哪些環(huán)節(jié)最有可能出問題,為什么?
我們都知道HashMap線程不安全辈毯,那么哪些環(huán)節(jié)最優(yōu)可能出問題呢坝疼,及其原因:沒有參照這個問題有點(diǎn)不好直接回答,但是我們可以找參照啊谆沃,參照:ConcurrentHashMap钝凶,因?yàn)榇蠹叶贾繦ashMap不是線程安全的,ConcurrentHashMap是線程安全的唁影,對照ConcurrentHashMap耕陷,看看ConcurrentHashMap在HashMap的基礎(chǔ)之上增加了哪些安全措施,這個問題就迎刃而解了据沈。后面會有分析ConcurrentHashMap的文章哟沫,這里先簡要回答這個問題:HashMap的put操作是不安全的,因?yàn)闆]有使用任何鎖锌介;HashMap在多線程下最大的安全隱患發(fā)生在擴(kuò)容的時候嗜诀,想想一個場合:HashMap使用默認(rèn)容量16,這時100個線程同時往HashMap中put元素孔祸,會發(fā)生什么隆敢?擴(kuò)容混亂,因?yàn)閿U(kuò)容也沒有任何鎖來保證并發(fā)安全崔慧,另外,后面的博文會講到ConcurrentHashMap的并發(fā)擴(kuò)容操作是ConcurrentHashMap的一個核心方法拂蝎。

HashMap的value允許為null,但是HashTable和ConcurrentHashMap的valued都不允許為null惶室,試分析原因温自?
首先要明確ConcurrentHashMap和Hashtable從技術(shù)從技術(shù)層面講是可以允許value為null;但是它是實(shí)際是不允許的皇钞,這肯定是為了解決一些問題悼泌,為了說明這個問題,我們看下面這個例子(這里以ConcurrentHashMap為例鹅士,HashTable也是類似)

HashMap由于允value為null券躁,get方法返回null時有可能是map中沒有對應(yīng)的key;也有可能是該key對應(yīng)的value為null掉盅。所以get不能判斷map中是否包含某個key也拜,只能使用contains判斷是否包含某個key。

看下面的代碼段趾痘,要求完成這個一個功能:如果map中包含了某個key則返回對應(yīng)的value慢哈,否則拋出異常:

if (map.containsKey(k)) {
   return map.get(k);
} else {
   throw new KeyNotPresentException();
}

如果上面的map為HashMap,那么沒什么問題永票,因?yàn)镠ashMap本來就是線程不安全的卵贱,如果有并發(fā)問題應(yīng)該用ConcurrentHashMap谜疤,所以在單線程下面可以返回正確的結(jié)果
如果上面的map為ConcurrentHashMap玩祟,此時存在并發(fā)問題:在map.containsKey(k)和map.get之間有可能其他線程把這個key刪除了,這時候map.get就會返回null,而ConcurrentHashMap中不允許value為null膘螟,也就是這時候返回了null姻几,一個根本不允許出現(xiàn)的值棉姐?
但是因?yàn)镃oncurrentHashMap不允許value為null工碾,所以可以通過map.get(key)是否為null來判斷該map中是否包含該key,這時就沒有上面的并發(fā)問題了踪央。


掃描下方二維碼臀玄,及時獲取更多互聯(lián)網(wǎng)求職面經(jīng)java畅蹂、python健无、爬蟲大數(shù)據(jù)等技術(shù)液斜,和海量資料分享
公眾號菜鳥名企夢后臺發(fā)送“csdn”即可免費(fèi)領(lǐng)取【csdn】和【百度文庫】下載服務(wù)累贤;
公眾號菜鳥名企夢后臺發(fā)送“資料”:即可領(lǐng)取5T精品學(xué)習(xí)資料java面試考點(diǎn)java面經(jīng)總結(jié)旗唁,以及幾十個java畦浓、大數(shù)據(jù)項(xiàng)目資料很全检疫,你想找的幾乎都有

掃碼關(guān)注,及時獲取更多精彩內(nèi)容祷嘶。(博主今日頭條大數(shù)據(jù)工程師)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末屎媳,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子论巍,更是在濱河造成了極大的恐慌烛谊,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嘉汰,死亡現(xiàn)場離奇詭異丹禀,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)鞋怀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進(jìn)店門双泪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人密似,你說我怎么就攤上這事焙矛。” “怎么了残腌?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵村斟,是天一觀的道長贫导。 經(jīng)常有香客問我,道長蟆盹,這世上最難降的妖魔是什么孩灯? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮逾滥,結(jié)果婚禮上钱反,老公的妹妹穿的比我還像新娘。我一直安慰自己匣距,他們只是感情好面哥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著毅待,像睡著了一般尚卫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上尸红,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天吱涉,我揣著相機(jī)與錄音,去河邊找鬼外里。 笑死怎爵,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的盅蝗。 我是一名探鬼主播鳖链,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼墩莫!你這毒婦竟也來了芙委?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤狂秦,失蹤者是張志新(化名)和其女友劉穎灌侣,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體裂问,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡侧啼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了堪簿。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片痊乾。...
    茶點(diǎn)故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖戴甩,靈堂內(nèi)的尸體忽然破棺而出符喝,到底是詐尸還是另有隱情,我是刑警寧澤甜孤,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布协饲,位于F島的核電站畏腕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏茉稠。R本人自食惡果不足惜描馅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望而线。 院中可真熱鬧铭污,春花似錦、人聲如沸膀篮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽誓竿。三九已至磅网,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間筷屡,已是汗流浹背涧偷。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留毙死,地道東北人燎潮。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像扼倘,于是被迫代替她去往敵國和親确封。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容