HashMap源碼分析

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

分析源碼之前饵蒂,先了解兩個數(shù)據(jù)結(jié)構(gòu)声诸,數(shù)組和鏈表。

1.1 數(shù)組
  • 內(nèi)存中分配固定的空間
  • 刪除或者增加退盯,導(dǎo)致數(shù)組下標(biāo)內(nèi)存位移彼乌,效率低
  • 數(shù)組大小固定不利于擴(kuò)增
  • 隨機(jī)讀取的效率高,因為數(shù)組是連續(xù)的渊迁,知道內(nèi)存地址慰照,可以直接獲取
Object []  arrays;
1.2 鏈表
  • 鏈表的內(nèi)存大小不固定
  • 刪除增加數(shù)據(jù)方便,因為每一個鏈表節(jié)點存儲了節(jié)點的數(shù)據(jù)和下一節(jié)點的內(nèi)存地址(單項或者雙向)
  • 查找數(shù)據(jù)時效率低宫纬,因為不具有隨機(jī)訪問性焚挠,所以訪問某個位置的數(shù)據(jù)都要從第一個數(shù)據(jù)開始訪問膏萧,然后根據(jù)第一個數(shù)據(jù)保存的下一個數(shù)據(jù)的地址找到第二個數(shù)據(jù)漓骚,以此類推。
  • 方便擴(kuò)容榛泛,增刪效率高
    Node {
        Object data;
        Node next;
        Node prex;
    }
1.3 集合特性

對于集合框架我們的關(guān)注點一般在一下幾點:

  1. 集合底層實現(xiàn)的數(shù)據(jù)結(jié)構(gòu)是什么 數(shù)組+鏈表+紅黑樹
  2. 集合中元素是否允許為空 key和value都可以為空
  3. 是否允許重復(fù)的數(shù)據(jù) key不可以蝌蹂,value可以
  4. 是否有序(這里的有序是指讀取數(shù)據(jù)和存放數(shù)據(jù)的順序是否一致) 否
  5. 是否線程安全。 否

2 HashMap

通過上面我們看到數(shù)組查詢效率高曹锨,增刪效率低孤个,而鏈表反之,那么問題來了沛简,有沒有一種結(jié)構(gòu)結(jié)合二者的優(yōu)點齐鲤?我們看下hashMap,直接上碼椒楣!

依賴關(guān)系

HashMap主要是繼承自AbstractMap给郊,實現(xiàn)了Cloneable和Serializable接口使得HashMap具有克隆和序列化的功能、實現(xiàn)了Map接口因此具有Map的性質(zhì)捧灰。

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

簡單寫個main淆九,put etc..

        HashMap map=new HashMap();
        map.put("c","123");

點擊put方法進(jìn)入源碼世界

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

看english注解,發(fā)現(xiàn)如果你put的key已經(jīng)存在的話,處理完會覆蓋之前的值炭庙,并且返回舊值饲窿,but putVal后面多了兩個參數(shù),先不管他,這個hash(key)有必要了解下焕蹄,go on

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

h>>>16代表右位移逾雄,低位溢出,符號位不變擦盾,并用符號位補(bǔ)溢出的高位嘲驾,那么看下hashCode()方法,打開Object類迹卢,可以看到

    public native int hashCode();//c++寫的貌似我看不到
    
    public int hashCode() {//這個是String類下重寫的
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
    
    public static int hashCode(int value) {//這個是Interger類重寫的
        return value;
    }
    //etc...

原來不同對象都可以重寫此方法辽故,獲取一串不唯一的數(shù)字,然后做右移操作腐碱,然而這個值來干啥呢誊垢?go on!

    /**
     * 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) {
                       
                   }

好症见,來到這里終于知道之前的兩個參數(shù)啥意思了喂走,putVal(hash(key), key, value, false, true); onlyIfAbsent if true, don't change existing value,evict if false, the table is in creation mode

        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

初始化為空的走擴(kuò)容谋作,下面著重看下resize方法,只考慮初始化芋肠,刪除其他的代碼

    final Node<K,V>[] resize() {  //通過這個方法也能看出,map的本質(zhì)是Node數(shù)組
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;//這個說明鬼遵蚜,接著往下看
        int newCap, newThr = 0;
        newCap = DEFAULT_INITIAL_CAPACITY;//點擊看默認(rèn)16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//這個是總大小*負(fù)載因子帖池,默認(rèn)0.75
        }
        threshold = newThr;
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//開辟數(shù)組
        table = newTab;
        return newTab;
    }

內(nèi)存有了,接著走吭净,根據(jù)hash取存儲下標(biāo)睡汹,如果這個地方?jīng)]有值,那就存數(shù)據(jù)

        if ((p = tab[i = (n - 1) & hash]) == null)//經(jīng)過高位運算的hash值和數(shù)組的大小-1取與寂殉,也就是說取值的范圍0-15也就是數(shù)組的下標(biāo)
            tab[i] = newNode(hash, key, value, null);
            
            ································································································································
            //在回顧下node的結(jié)構(gòu)囚巴,單鏈表
            Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;//hash在這里
            this.key = key;
            this.value = value;
            this.next = next;
            }
        

那么問題來了hash值一樣了怎么辦? 怎么會一樣友扰,比如String a彤叉,Interger 97

        HashMap map=new HashMap();
        map.put(new String("a"),"a");
        map.put(new Integer(97),97);
        //String hashCode 方法
        public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
        }
        //Interger hashCode 方法
        public static int hashCode(int value) {
        return value;
        }
        

如果存在相同的hash值,那么第二個來的時候必然已經(jīng)存在

            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p; //如果key是同一個對象村怪,把存在的節(jié)點賦值給e秽浇,那值怎么辦?別著急賦值在后面
            else if (p instanceof TreeNode)//1.8新增的一種結(jié)構(gòu)实愚,稍后看
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                 //這里就是解決hash沖突的地方了
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        
                        if (binCount >= TREEIFY_THRESHOLD - 1) // 多了個限制兼呵,防止鏈表無限擴(kuò)充吧兔辅,稍后分析
                            treeifyBin(tab, hash);
                        break;
                    }
                    //鏈表已經(jīng)存在沖突的hash數(shù)據(jù),如果key對象和再次傳過來的一樣击喂,直接返回當(dāng)前node的節(jié)點维苔,即更改引用。
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // 最后復(fù)制操作來了
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)//開關(guān)+以前的數(shù)據(jù)為空懂昂,就覆蓋
                    e.value = value; 
                afterNodeAccess(e);
                return oldValue;
            }

上面代碼可以看到遍歷當(dāng)前的Node節(jié)點介时,看他的下一個是不是存在,如果不存在凌彬,就放在他下一個的位置
那么極端情況沸柔,hash一直沖突,Node就可以無限增長了嗎铲敛,肯定是不可以的褐澎。
TREEIFY_THRESHOLD 看下源碼大小限制為8,超過了走treeifyBin(),然后再看下treeifyBin方法

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();//如果小于64繼續(xù)擴(kuò)容
        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);
        }
    }

超過64轉(zhuǎn)樹伐蒋,小于64通過擴(kuò)容解決工三,先瞧瞧不到64繼續(xù)擴(kuò)容會怎樣,繼續(xù)看下resize方法的實現(xiàn)

        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
        }

進(jìn)行擴(kuò)容oldCap << 1 位運算增加一倍先鱼,擴(kuò)容怎么能解決hash沖突俭正?繼續(xù)往下go
最下面,循環(huán)遍歷舊的數(shù)組焙畔,針對沒有子節(jié)點的鏈表掸读,重新計算index的位置,賦值宏多。

                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

接著針對有子節(jié)點的Node

    /**
     JDK1.7中儿惫,resize時,index取得時绷落,全部采用重新hash的方式進(jìn)行了姥闪。JDK1.8對這個進(jìn)行了改善始苇。
     以前要確定index的時候用的是(e.hash&oldCap-1)砌烁,是取模取余,而這里用到的是(e.hash &oldCap)
     它有兩種結(jié)果催式,一個是0函喉,一個是oldCap,
     比如oldCap=8,hash是3荣月,11管呵,19,27時哺窄,
     (e.hash & oldCap)的結(jié)果是0捐下,8账锹,0,8缺谴,這樣3彬碱,19組成新的鏈表鹉梨,index為3
     而11,27組成新的鏈表廓奕,新分配的index為3+8;
     JDK1.7中重寫hash是(e.hash & newCap-1)档叔,也就是3桌粉,11,19衙四,27對16取余铃肯,也是3,11传蹈,3缘薛,11,
     和上面的結(jié)果一樣卡睦,但是index為3的鏈表是19宴胧,3,index為3+8的鏈表是27表锻,11恕齐,
     也就是說1.7中經(jīng)過resize后數(shù)據(jù)的順序變成了倒敘,而1.8沒有改變順序瞬逊。
    **/
    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;
    }

也就是說針對e.hash & oldCap=0的保持原來的索引位置不變显歧,為1的原索引位置+原數(shù)組的大小,這也是是1.8針對1.7做的優(yōu)化确镊∈恐瑁回過頭來再分析下如果數(shù)組Node總大小大于64了會怎樣?

        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);
        }

接下來分析不下去了蕾域,因為要補(bǔ)下紅黑樹的知識點拷肌。。旨巷。先分析下get接口巨缘,關(guān)鍵代碼如下

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        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;//直接找到返回value值
            if ((e = first.next) != null) {//出現(xiàn)沖突,遍歷節(jié)點
        
                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;
    }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末采呐,一起剝皮案震驚了整個濱河市若锁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌斧吐,老刑警劉巖又固,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件仲器,死亡現(xiàn)場離奇詭異,居然都是意外死亡仰冠,警方通過查閱死者的電腦和手機(jī)娄周,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沪停,“玉大人煤辨,你說我怎么就攤上這事∧菊牛” “怎么了众辨?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長舷礼。 經(jīng)常有香客問我鹃彻,道長,這世上最難降的妖魔是什么妻献? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任蛛株,我火速辦了婚禮,結(jié)果婚禮上育拨,老公的妹妹穿的比我還像新娘谨履。我一直安慰自己,他們只是感情好熬丧,可當(dāng)我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布笋粟。 她就那樣靜靜地躺著,像睡著了一般析蝴。 火紅的嫁衣襯著肌膚如雪害捕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天闷畸,我揣著相機(jī)與錄音尝盼,去河邊找鬼。 笑死佑菩,一個胖子當(dāng)著我的面吹牛盾沫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播倘待,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼疮跑,長吁一口氣:“原來是場噩夢啊……” “哼组贺!你這毒婦竟也來了凸舵?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤失尖,失蹤者是張志新(化名)和其女友劉穎啊奄,沒想到半個月后渐苏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡菇夸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年琼富,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片庄新。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡鞠眉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出择诈,到底是詐尸還是另有隱情械蹋,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布羞芍,位于F島的核電站哗戈,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏荷科。R本人自食惡果不足惜唯咬,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望畏浆。 院中可真熱鬧胆胰,春花似錦、人聲如沸刻获。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽将鸵。三九已至勉盅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間顶掉,已是汗流浹背草娜。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留痒筒,地道東北人宰闰。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像簿透,于是被迫代替她去往敵國和親移袍。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,612評論 2 350

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

  • HashMap 是 Java 面試必考的知識點老充,面試官從這個小知識點就可以了解我們對 Java 基礎(chǔ)的掌握程度葡盗。網(wǎng)...
    野狗子嗷嗷嗷閱讀 6,661評論 9 107
  • HashMap HashMap概述 HashMap基于哈希表的 Map 接口的實現(xiàn)。此實現(xiàn)提供所有可選的映射操作啡浊,...
    史路比閱讀 291評論 0 6
  • 一觅够、HashMap概述 HashMap基于哈希表的Map接口的實現(xiàn)胶背。此實現(xiàn)提供所有可選的映射操作,并允許使用nul...
    小陳阿飛閱讀 633評論 0 2
  • Java是一種可以撰寫跨平臺應(yīng)用軟件的面向?qū)ο蟮某绦蛟O(shè)計語言喘先。Java 技術(shù)具有卓越的通用性钳吟、高效性、平臺移植性和...
    Java小辰閱讀 229評論 0 0
  • 人長了這么多年窘拯,一點一點走到今天這個地步红且,滿心以為自己懂得了很多。 聽母親講話的時候習(xí)慣性地說涤姊,這些道理我都懂啦直焙,...
    涼已閱讀 417評論 -2 1