HashMap源碼解讀

1. 數(shù)據(jù)結(jié)構(gòu)

在Java中巩搏,是通過數(shù)組和鏈表這倆種數(shù)據(jù)結(jié)構(gòu)來進行數(shù)據(jù)存儲的。

  • 數(shù)組:
  1. 數(shù)組的存儲空間的連續(xù)的说敏,所以占內(nèi)存嚴(yán)重
  2. 二分查找復(fù)雜度小
  3. 尋址容易诗赌,插入刪除難
  • 鏈表:
  1. 區(qū)間離散,占用內(nèi)存比較寬松
  2. 空間復(fù)雜度很小被饿,時間復(fù)雜度很大
  3. 尋址難四康,插入刪除容易

哈希表(Hash Table)就是這兩者的結(jié)合,插入刪除簡單锹漱,尋址容易箭养,占用內(nèi)存相對寬松。

  • 拉鏈法(鏈地址法):鏈表的數(shù)組
拉鏈法

最常用的哈希表排列法哥牍,一目了然的數(shù)組+鏈表組合毕泌。
存儲規(guī)則:整個數(shù)組長度為16,每個元素存儲的是一個鏈表的頭結(jié)點嗅辣。比如:1%16 = 1撼泛,337%16 = 1,353%16 = 1 澡谭。余數(shù)皆為1愿题,所以這三個元素被存儲在數(shù)組下標(biāo)為1的位置。

解決哈希沖突:同數(shù)組下標(biāo)下蛙奖,通過鏈?zhǔn)浇Y(jié)構(gòu)避免索引值沖突潘酗。

而HashMap就是典型的拉鏈法哈希表結(jié)構(gòu)。

他實際上是一個線性數(shù)組雁仲。他的靜態(tài)內(nèi)部類繼承了一個Entry接口仔夺。這里注意,在jdk1.8中攒砖,在鏈表中加入了紅黑樹(平衡二分查找樹)缸兔。所以1.8版本的HashMap是由數(shù)組+(鏈表+紅黑樹)實現(xiàn)的。

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    ......
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//hash code value for this map entry
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
    ......
}

乍一眼就能看出是一個bean類吹艇,關(guān)鍵的參數(shù)有三個:key惰蜜,value,next受神,接下來我們看下HashMap的方法與Entry的關(guān)聯(lián)

  1. get方法
public V get(Object key) {
   Node<K,V> e;
   return (e = getNode(hash(key), key)) == null ? null : e.value;//key是否存在抛猖,存在返回key的value值,不存在返回null
  //hash(key)獲得key的hash值
}

getNode方法


    /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; //Entry數(shù)組
        Node<K,V> first, e; 
        int n; //數(shù)組長度
        K k;
        // 1. 定位鍵值對所在桶的位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //2.判斷鍵值的hashcode相等,對象相等
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                // 3..如果 first 是 TreeNode 類型财著,則調(diào)用黑紅樹查找方法
                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;
    }
  1. 我們看一下判斷條件养交。前兩個是tab非空判斷,沒什么好說的瓢宦,看下第三個條件。
    (first = tab[(n - 1) & hash]) != null其中

    tab[(n - 1) & hash]這個可能有點難理解灰羽。這里是為了運算出桶在桶數(shù)組中的位置驮履。HashMap 中桶數(shù)組的大小 length 總是2的冪,此時廉嚼,(n - 1) & hash 等價于對 length 取余玫镐。
    隨便假設(shè)一對值,比如hash = 98怠噪,n = 21 恐似,98 & (21-1) = 0,是不是和下方圖示運算結(jié)果一樣傍念?位運算效率是高于運算符的矫夷,這算是java優(yōu)化中的小細節(jié)。

位運算取余數(shù)
  1. 這里需注意憋槐,hashcode相同不代表是同一個對象双藕,只有equals才能判斷兩個是否是同一對象(鍵值)
  1. 黑紅樹是jdk1.8在HashMap的性能優(yōu)化中新添加的特性。它主要有五大性質(zhì):
  • 節(jié)點是紅色或黑色阳仔。
  • 根是黑色忧陪。
  • 所有葉子都是黑色(葉子是NIL節(jié)點)。
  • 每個紅色節(jié)點必須有兩個黑色的子節(jié)點近范。(從每個葉子到根的所有路徑上不能有兩個連續(xù)的紅色節(jié)點嘶摊。)
  • 從任一節(jié)點到其每個葉子的所有簡單路徑都包含相同數(shù)目的黑色節(jié)點(簡稱黑高)。
紅黑樹
  1. put方法
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);//1. onlyIfAbsent參數(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
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {

        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 初始化桶數(shù)組 table
        if ((tab = table) == null || (n = tab.length) == 0)
            //擴容方法
            n = (tab = resize()).length;
        // 當(dāng)前key不存在评矩,新建鍵值對加入
        if ((p = tab[i = (n - 1) & hash]) == null)

            tab[i] = newNode(hash, key, value, null);

        else {
            Node<K,V> e; K k;
            // 如果鍵的值以及節(jié)點 hash 等于鏈表中的第一個鍵值對節(jié)點時叶堆,則將 e 指向該鍵值對
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果節(jié)點下引用數(shù)據(jù)結(jié)構(gòu)為紅黑樹,調(diào)用紅黑樹插入法
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 鏈表結(jié)構(gòu)稚照,遍歷
                for (int binCount = 0; ; ++binCount) {
                    //不存在當(dāng)前需要插入的節(jié)點  
                    if ((e = p.next) == null) {
                        //新建一個節(jié)點插入
                        p.next = newNode(hash, key, value, null);
                        //鏈表長度超過或等于樹化闕值(8)蹂空,對鏈表進行樹化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //需要插入的節(jié)點已經(jīng)存在了
                    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)//1.onlyIfAbsent 判斷
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
  1. onlyIfAbsent這個參數(shù),牽扯到另外一個put方法:putIfAbsent方法
  public V putIfAbsent(K key, V value) {
        return putVal(hash(key), key, value, true, true);
  }

他和put方法唯一的區(qū)別就是onlyIfAbsent的值為true

特點:putIfAbsent在放入數(shù)據(jù)時果录,如果存在重復(fù)的key上枕,那么putIfAbsent不會放入值。
如果傳入key對應(yīng)的value已經(jīng)存在弱恒,就返回存在的value辨萍,不進行替換。如果不存在,就添加key和value锈玉,返回null

  1. resize方法

resize(),咱們稱之為擴容方法爪飘,只有在兩種情況下會被調(diào)用:

  • HashMap實行了懶加載: 新建HashMap時不會對table進行賦值, 而是到第一次插入時, 進行resize時構(gòu)建table;
  • 當(dāng)HashMap的size值大于 threshold時, 會進行resize(); 看一下threshold在源碼中的注解:
// (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.)
   int threshold;

  /**
   * The default initial capacity - MUST be a power of two.
   */
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

1 << 4是位運算,結(jié)果threshold默認值為16拉背。

resize()方法源碼:

   /**
     * 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;//將當(dāng)前table暫存到oldtab來操作

        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;//閾值設(shè)置為Integer的最大值师崎,好像是2147483647,遠大于默認的最大容量

                return oldTab;//直接返回當(dāng)前table椅棺,不用擴容

            }

            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

                     oldCap >= DEFAULT_INITIAL_CAPACITY)

                newThr = oldThr << 1; // 雙倍擴大老內(nèi)存和老閾值并賦給新的table

        }

        else if (oldThr > 0) // initial capacity was placed in threshold

            newCap = oldThr;

        else {               //這種情況是初始化HashMap時啥參數(shù)都沒加

            newCap = DEFAULT_INITIAL_CAPACITY;

            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

        }

        if (newThr == 0) {//當(dāng)只滿足老閾值大于0的條件時犁罩,新閾值等于新容量*默認擴容因子

            float ft = (float)newCap * loadFactor;

            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

                      (int)ft : Integer.MAX_VALUE);

        }

        threshold = newThr;//把新的閾值賦給當(dāng)前table

        @SuppressWarnings({"rawtypes","unchecked"})

            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//創(chuàng)建容量為newCap的新table

        table = newTab;

        if (oldTab != null) {

            for (int j = 0; j < oldCap; ++j) {//對老table進行遍歷

                Node<K,V> e;

                if ((e = oldTab[j]) != null) {//遍歷到的賦給e進行暫存,同時將老table對應(yīng)項賦值為null

                    oldTab[j] = null;

                    if (e.next == null)//將不為空的元素復(fù)制到新table中

                        newTab[e.hash & (newCap - 1)] = e;//等于是創(chuàng)建一個新的空table然后重新進行元素的put两疚,這里的table長度是原table的兩倍

                    else if (e instanceof TreeNode)//暫時沒了解紅黑樹

                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                    else { // preserve order

                        Node<K,V> loHead = null, loTail = null;//用于保存put后不移位的鏈表

                        Node<K,V> hiHead = null, hiTail = null;//用于保存put后移位的鏈表

                        Node<K,V> next;

                        do {

                            next = e.next;

                            if ((e.hash & oldCap) == 0) {//如果與的結(jié)果為0床估,表示不移位,將桶中的頭結(jié)點添加到lohead和lotail中诱渤,往后如果桶中還有不移位的結(jié)點丐巫,就向tail繼續(xù)添加

                                if (loTail == null)//在后面遍歷lohead和lotail保存到table中時,lohead用于保存頭結(jié)點的位置勺美,lotail用于判斷是否到了末尾

                                    loHead = e;

                                else

                                    loTail.next = e;

                                loTail = e;

                            }

                            else {//這是添加移位的結(jié)點递胧,與不移位的類似

                                if (hiTail == null)

                                    hiHead = e;

                                else

                                    hiTail.next = e;

                                hiTail = e;

                            }

                        } while ((e = next) != null);

                        if (loTail != null) {//把不移位的結(jié)點添加到對應(yīng)的鏈表數(shù)組中去

                            loTail.next = null;

                            newTab[j] = loHead;

                        }

                        if (hiTail != null) {//把移位的結(jié)點添加到對應(yīng)的鏈表數(shù)組中去

                            hiTail.next = null;

                            newTab[j + oldCap] = hiHead;

                        }

                    }

                }

            }

        }
        return newTab;
    }

4.線程安全
HashMap是線程不安全的,但是HashTable由于每次put操作就進行鎖死效率十分低下赡茸。那有沒有既效率又線程安全的HashMap呢谓着?

答案就是:ConcurrentHashMap

  • 底層采用分段的數(shù)組+鏈表實現(xiàn),線程安全
  • 通過把整個Map分為N個Segment坛掠,可以提供相同的線程安全赊锚,但是效率提升N倍,默認提升16倍屉栓。(讀操作不加鎖舷蒲,由于HashEntry的value變量是 volatile的,也能保證讀取到最新的值友多。)
  • Hashtable的synchronized是針對整張Hash表的牲平,即每次鎖住整張表讓線程獨占,ConcurrentHashMap允許多個修改操作并發(fā)進行域滥,其關(guān)鍵在于使用了鎖分離技術(shù)
  • 有些方法需要跨段纵柿,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段启绰,這需要按順序鎖定所有段昂儒,操作完畢后,又按順序釋放所有段的鎖
  • 擴容:段內(nèi)擴容(段內(nèi)元素超過該段對應(yīng)Entry數(shù)組長度的75%觸發(fā)擴容委可,不會對整個Map進行擴容)渊跋,插入前檢測需不需要擴容,有效避免無效擴容

參考:https://www.cnblogs.com/heyonggang/p/9112731.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市拾酝,隨后出現(xiàn)的幾起案子燕少,更是在濱河造成了極大的恐慌,老刑警劉巖蒿囤,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件客们,死亡現(xiàn)場離奇詭異,居然都是意外死亡材诽,警方通過查閱死者的電腦和手機镶摘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來岳守,“玉大人,你說我怎么就攤上這事碌冶∈。” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵扑庞,是天一觀的道長譬重。 經(jīng)常有香客問我,道長罐氨,這世上最難降的妖魔是什么臀规? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮栅隐,結(jié)果婚禮上塔嬉,老公的妹妹穿的比我還像新娘。我一直安慰自己租悄,他們只是感情好谨究,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著泣棋,像睡著了一般胶哲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上潭辈,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天鸯屿,我揣著相機與錄音,去河邊找鬼把敢。 笑死寄摆,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的修赞。 我是一名探鬼主播冰肴,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了熙尉?” 一聲冷哼從身側(cè)響起联逻,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎检痰,沒想到半個月后包归,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡铅歼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年公壤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片椎椰。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡厦幅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出慨飘,到底是詐尸還是另有隱情确憨,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布瓤的,位于F島的核電站休弃,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏圈膏。R本人自食惡果不足惜塔猾,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望稽坤。 院中可真熱鬧丈甸,春花似錦、人聲如沸尿褪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽茫多。三九已至祈匙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間天揖,已是汗流浹背夺欲。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留今膊,地道東北人些阅。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像斑唬,于是被迫代替她去往敵國和親市埋。 傳聞我的和親對象是個殘疾皇子黎泣,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

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

  • 前文:HashMap是Java程序員最常用的映射(鍵值對)處理數(shù)據(jù)的容器。隨著JDK版本的更新缤谎,1.8相較于1.7...
    是一動不動的friend閱讀 1,220評論 0 1
  • 前言: 本文基于 JDK1.8抒倚,不會過多的擴展其它知識,重點關(guān)注 HashMap 的實現(xiàn)坷澡。 首先簡單介紹一下和 H...
    M_lear閱讀 2,244評論 0 3
  • HashMap概述 Hash托呕,又稱散列。哈希表是一種以鍵-值(key-value) 存儲數(shù)據(jù)的频敛,和數(shù)組项郊、鏈表、二叉...
    99793933e682閱讀 560評論 0 6
  • 本文分析HashMap的實現(xiàn)原理斟赚。 數(shù)據(jù)結(jié)構(gòu)(散列表) HashMap是一個散列表(也叫哈希表)着降,用來存儲鍵值對(...
    developerzjy閱讀 1,299評論 1 21
  • 哈哈這款粥非常適合怕麻煩的我。 食材很簡單:水 大米 蝦米 火腿 工具更簡單:電飯煲 耗時四十五分鐘拗军,我用的...
    晴杰閱讀 447評論 2 1