Java Map 淺析之 HashMap

Map接口下主要介紹HashMap,TreeMap。HashMap與Hashtable關(guān)系跟ArrayList與Vector關(guān)系類似馁痴。ConcurrentHashMap相比于hashtable做的優(yōu)化疯搅。Hashtable是遺留類挠铲,很多常用功能與HashMap類似,不同的是它承自Dictionary類萨驶,并且是線程安全的代虾,任一時間只有一個線程能寫Hashtable进肯。Hashtable并發(fā)性不如ConcurrentHashMap,因為ConcurrentHashMap引入了分段鎖棉磨。Hashtable不建議在新代碼中使用江掩,不需要線程安全的場合可以用HashMap替換,需要線程安全的場合可以用ConcurrentHashMap替換乘瓤。

HashMap

首先大家可以可以在腦子里面有個模糊的概念频敛。HashMap大概的結(jié)構(gòu)是怎么樣的,又便于之后理解馅扣。當然Java7和Java8的內(nèi)部實現(xiàn)是不一樣的。下面先按Java7去分析理解

Java7 - HashMap

    //可以看出Entry為單向鏈表
   static class Entry<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Entry<K,V> next;
   }
//以下為HashMap的一些成員變量和常量
//內(nèi)部存儲結(jié)構(gòu)着降,初次使用時創(chuàng)建和后續(xù)增長差油。
    transient Node<K,V>[] table;
// HashMap的大小,它是HashMap保存的鍵值對的數(shù)量
    transient int size;
//被修改次數(shù)任洞,使用于fail-fast機制
    transient int modCount;
 // HashMap的閾值蓄喇,用于判斷是否需要調(diào)整HashMap的容量(threshold = 容量*加載因子)
    int threshold;
// 加載因子實際大小
    final float loadFactor;     //默認0.75
// 默認的初始容量是16,必須是2的冪交掏。
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量(必須是2的冪且小于2的30次方妆偏,傳入容量過大將被這個值替換)
    static final int MAXIMUM_CAPACITY = 1 << 30;
// 默認加載因子,0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

由代碼可以看出盅弛,大方向上钱骂,HashMap 里面是一個數(shù)組叔锐,然后數(shù)組中每個元素是一個單向鏈表。
然后我們從最常用的put方法開始见秽,跟蹤源碼并分析

public V put(K key, V value) {
    // 當插入第一個元素的時候愉烙,需要先初始化數(shù)組大小
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 如果 key 為 null,最終會將 entry 放到 table[0] 中
    if (key == null)
        return putForNullKey(value);
    //開始插入步驟
    // 1. 求 key 的 hash 值
    int hash = hash(key);
    // 2. 找到對應(yīng)的數(shù)組下標
    int i = indexFor(hash, table.length);
    // 3. 遍歷一下對應(yīng)下標[i]處的鏈表解取,看是否有重復(fù)的 key 已經(jīng)存在步责,
    //    如果有,直接覆蓋禀苦,put 方法返回舊值就結(jié)束了
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
 
    modCount++;
    // 4. 不存在重復(fù)的 key蔓肯,將此 entry 添加到鏈表中
    addEntry(hash, key, value, i);
    return null;
}

由put方法可以看出,可以大致分為三種情況振乏。

  • key為null -> 放置于table[0] 中
  • hash已存在 -> 在對應(yīng)下標鏈表添加/覆蓋(覆蓋時put方法返回舊值)
  • hash未存在 -> 將entry添加到table中(可能會出現(xiàn)resize情況)

我們從put方法一個個節(jié)點去分析蔗包。首先table == EMPTY_TABLE時的初始化過程。

    //若表未初始化過昆码,會調(diào)用這個方法去初始化气忠。
    private void inflateTable(int toSize) {
        // 保證容器為2次冪大小
        int capacity = roundUpToPowerOf2(toSize);

        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

可以看出初始化后的容器大小為2次冪大小且為向上取整。為什么時要2次冪等下會說明赋咽。初始化完容器后旧噪,會判斷entry的key是否為null,為null由putForNullKey放置進容器中脓匿。putForNullKey方法會判斷在table[0]中是否有key==null淘钟,存在則直接替換,不存在通過 addEntry(0, null, value, 0)加入對應(yīng)entry進table[0]中陪毡。所以只允許一個key為null的元素米母。

//in jdk1.7.0_51
    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    static int indexFor(int h, int length) {
        return h & (length-1);
    }

這里可以看出容器大小必須為2次冪的原因有兩個。

  • 保證分布均勻(計算hash時會有格外的擾動毡琉,保證均勻)
  • 當大小為2次冪時铁瞒,相對耗時的%操作可以替換成相對效率更高的位運算代替。因為當容量一定是2^n時桅滋,h & (length - 1) == h % length
    這里可以看到j(luò)ava7的時候HashMap的hash值是經(jīng)過了4次擾動的慧耍。而java8則只經(jīng)過一次擾動。
    void addEntry(int hash, K key, V value, int bucketIndex) {
        //如果當前 HashMap 大小已經(jīng)達到了閾值丐谋,并且新值要插入的數(shù)組位置已經(jīng)有元素了芍碧,那么要擴容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0; //重新計算hash值
            bucketIndex = indexFor(hash, table.length);//重新獲取bucketIndex
        }

        createEntry(hash, key, value, bucketIndex);
    }
    //將新值放到鏈表的表頭,然后 size++
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

可以看到添加元素時号俐,如果當前 HashMap 大小已經(jīng)達到了閾值泌豆,并且新值要插入的數(shù)組位置已經(jīng)有元素了,那么要擴容(為原來2倍吏饿,如果沒有超過最大值時)踪危。

    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        //轉(zhuǎn)移對應(yīng)元素
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

擴容時蔬浙,將通過transfer方法將原來 table[i] 中的鏈表的所有節(jié)點,分拆到新的數(shù)組的 newTable[i] 和 newTable[i + oldLength] 位置上陨倡。因為擴容2倍之后indexFor返回值就發(fā)生改變了敛滋,假設(shè)原本15%15==0現(xiàn)在則時15%31==15了。

這樣子put方法分析完兴革,get方法就簡單了绎晃。

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        //計算hash值
        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

getForNullKey()方法直接在table[0]中鏈表判斷key值是否存在,就不展開了杂曲。
getEntry()方法耶比較簡單庶艾,大概分為三步:

  • 根據(jù) key通過hash()計算 hash 值。
  • 通過indexFor()找到相應(yīng)的數(shù)組下標:hash & (length – 1)擎勘。
  • 遍歷該數(shù)組位置處的鏈表咱揍,直到找到相等(==或equals)的 key。

Java8 HashMap

java8后棚饵,HashMap進行了優(yōu)化煤裙。引入了紅黑樹的結(jié)構(gòu)。當鏈表長度大于8之后噪漾,鏈表將轉(zhuǎn)化為紅黑樹硼砰,減低查詢時間復(fù)雜度O(logn)。
我們還是從put方法開始

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

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

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //與java7一樣欣硼,第一次進行初始化题翰。
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //對應(yīng)下標[ (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;
            //第一個節(jié)點key值與插入key"一致",取出該值诈胜,直接覆蓋
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)  //為紅黑樹豹障,通過紅黑樹操作設(shè)值(不展開)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else { //為鏈表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) { //插到鏈表最后
                        p.next = newNode(hash, key, value, null);
                        //鏈表長度大于8轉(zhuǎn)換為紅黑樹進行處理
                        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))))
                        break;
                    p = e;
                }
            }
            // 根據(jù)onlyIfAbsent判斷是否直接覆蓋焦匈。oldValue == null直接覆蓋
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 如果 HashMap 由于新插入這個值導(dǎo)致 size 已經(jīng)超過了閾值血公,需要進行擴容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

在hash(Object key)方法在java8中進行了優(yōu)化,僅進行了一次擾動缓熟,而java7進行了4次擾動坞笙。在美團技術(shù)團隊就做了比較好的分析。java8雖然只是一次擾動荚虚,但是將高位與低位相對隨機的混淆,保證了在table較小時籍茧,高位bit也能參與到低位運算中去版述。
與put方法對比,java7中為先判斷是否需要擴容在添加寞冯,而java8為先插入再判斷是否需要擴容渴析。
在java7中晚伙,因為插入在擴容之后,插入前table中元素已經(jīng)按擴容后的位置排序俭茧,所以插入時按新的IndexFor()插入即可咆疗。

//in java8
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        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;
            }//  容量未超過最大值,則容量及閾值翻倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;// 賦值新的數(shù)組母债,初始化則將直接返回 newTab
        if (oldTab != null) {
            //將每個bucket遷移到新的buckets
            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;
                            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;
    }

可以看到resize還是將容量直接翻倍,但是翻倍后對原數(shù)據(jù)處理不一樣了毡们。
先看鏈表處理迅皇,這里將舊鏈表分為lo鏈表(原位置鏈表)和hi鏈表(原位置+oldCap鏈表),原數(shù)據(jù)應(yīng)該放置于那個鏈表由(e.hash & oldCap) == 0決定衙熔。(e.hash & oldCap) == 0是等價于原index登颓,而(e.hash & oldCap) == 1則等價于原index+oldCap。因為oldCap為2次冪红氯,所以oldCap可以作為一個掩碼框咙。可以參考java8對Hashmap的改進
紅黑樹處理這里就不說了痢甘。喇嘱。。比較看不懂产阱。婉称。挖坑,找個時間理解下紅黑樹相關(guān)操作再補充

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末构蹬,一起剝皮案震驚了整個濱河市王暗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌庄敛,老刑警劉巖俗壹,帶你破解...
    沈念sama閱讀 212,686評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異藻烤,居然都是意外死亡绷雏,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,668評論 3 385
  • 文/潘曉璐 我一進店門怖亭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涎显,“玉大人,你說我怎么就攤上這事兴猩∑谙牛” “怎么了?”我有些...
    開封第一講書人閱讀 158,160評論 0 348
  • 文/不壞的土叔 我叫張陵倾芝,是天一觀的道長讨勤。 經(jīng)常有香客問我箭跳,道長,這世上最難降的妖魔是什么潭千? 我笑而不...
    開封第一講書人閱讀 56,736評論 1 284
  • 正文 為了忘掉前任谱姓,我火速辦了婚禮,結(jié)果婚禮上刨晴,老公的妹妹穿的比我還像新娘屉来。我一直安慰自己,他們只是感情好割捅,可當我...
    茶點故事閱讀 65,847評論 6 386
  • 文/花漫 我一把揭開白布奶躯。 她就那樣靜靜地躺著,像睡著了一般亿驾。 火紅的嫁衣襯著肌膚如雪嘹黔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,043評論 1 291
  • 那天莫瞬,我揣著相機與錄音儡蔓,去河邊找鬼。 笑死疼邀,一個胖子當著我的面吹牛喂江,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播旁振,決...
    沈念sama閱讀 39,129評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼获询,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拐袜?” 一聲冷哼從身側(cè)響起吉嚣,我...
    開封第一講書人閱讀 37,872評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蹬铺,沒想到半個月后尝哆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,318評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡甜攀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,645評論 2 327
  • 正文 我和宋清朗相戀三年秋泄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片规阀。...
    茶點故事閱讀 38,777評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡恒序,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谁撼,到底是詐尸還是另有隱情奸焙,我是刑警寧澤,帶...
    沈念sama閱讀 34,470評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站与帆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏墨榄。R本人自食惡果不足惜玄糟,卻給世界環(huán)境...
    茶點故事閱讀 40,126評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望袄秩。 院中可真熱鬧阵翎,春花似錦、人聲如沸之剧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,861評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽背稼。三九已至贰军,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蟹肘,已是汗流浹背词疼。 一陣腳步聲響...
    開封第一講書人閱讀 32,095評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留帘腹,地道東北人贰盗。 一個月前我還...
    沈念sama閱讀 46,589評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像阳欲,于是被迫代替她去往敵國和親舵盈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,687評論 2 351

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

  • 前言 這次我和大家一起學(xué)習(xí)HashMap球化,HashMap我們在工作中經(jīng)常會使用秽晚,而且面試中也很頻繁會問到,因為它里...
    liangzzz閱讀 7,974評論 7 102
  • HashMap 是 Java 面試必考的知識點赊窥,面試官從這個小知識點就可以了解我們對 Java 基礎(chǔ)的掌握程度爆惧。網(wǎng)...
    野狗子嗷嗷嗷閱讀 6,662評論 9 107
  • MVCC就是在不同的隔離級別下 建立不同的 readview 比如在可重復(fù)讀的隔離級別下, readview就是在...
    wuyuan0127閱讀 200評論 0 0
  • 天心一炁閱讀 344評論 0 0
  • 【今日讀書】《禪與摩托車維修藝術(shù)》 【今日讀書時間】16:00-18:15 【閱讀總結(jié)】本文的作者羅伯特?M.波西...
    Gyokusei閱讀 128評論 0 0