Java集合框架源碼解讀(2)——HashMap

Java集合框架源碼解讀(2)——HashMap

在Java Collections Framework的體系中中赦肋,主要有兩個(gè)重要的接口块攒,一個(gè)是List、Set和Queue所屬的Collection佃乘,還有一個(gè)就是Map接口了囱井。在上一篇文章中介紹了List接口,它適用于按數(shù)值索引訪問元素的情形趣避。本文中將介紹的Map則提供了一個(gè)更通用的元素存儲(chǔ)方法庞呕。

Map 集合類用于存儲(chǔ)元素對(稱作“鍵”和“值”)也叫鍵值對(key/value pair),其中每個(gè)鍵映射到一個(gè)值程帕。從概念上而言住练,你可以將 List 看作是具有數(shù)值鍵的 Map。Map接口規(guī)定key值是不能重復(fù)的骆捧,而value值可以重復(fù)澎羞。

Map接口有三種重要的具體實(shí)現(xiàn)類——HashMap髓绽、WeakHashMapTreeMap敛苇,其中HashMap還有一個(gè)重要的子類LinkedHashMap,它們都是非線程安全的類顺呕,本文將通過分析源碼重點(diǎn)介紹HashMap類枫攀,關(guān)于另外幾個(gè)類的內(nèi)容則留到后續(xù)文章再講。

Map接口的架構(gòu)如下圖所示:

img

在圖中可以看到株茶,Map接口還有一個(gè)叫做HashTable的實(shí)現(xiàn)類来涨,它是JDK早期的產(chǎn)物,與HashMap實(shí)現(xiàn)基本相似启盛,不過是它是線程安全的蹦掐,由于該容器已經(jīng)過時(shí)技羔,現(xiàn)在基本被棄用,因此在系列文章中就不多加筆墨去介紹了卧抗。

概述

HashMap是基于哈希表實(shí)現(xiàn)的藤滥,HashMap的每一個(gè)元素是一個(gè)key-value對,其內(nèi)部通過單鏈表紅黑樹解決沖突問題社裆,容量不足時(shí)會(huì)自動(dòng)擴(kuò)容拙绊。

HashMap是非線程安全的,只適用于單線程環(huán)境下泳秀,多線程環(huán)境下可以采用Concurrent并發(fā)包下的ConcurrentHashMap

哈希沖突

對于每個(gè)對象 X 和 Y,如果當(dāng)且僅當(dāng) X.equals(Y) 為 false卦睹,使得 X.hashCode()!= Y.hashCode() 為 true逗物,這樣的函數(shù)叫做完美 Hash 函數(shù)。當(dāng)哈希函數(shù)對兩個(gè)不同的數(shù)據(jù)項(xiàng)產(chǎn)生了相同的hash值時(shí)磺陡,這就稱為哈希沖突趴梢。

基于對象中變化的字段,我們可以很容易地構(gòu)造一個(gè)完美哈希函數(shù)币他,但是這需要無限的內(nèi)存大小坞靶,這種假設(shè)顯然是不可能的。而且蝴悉,即使我們能夠?yàn)槊總€(gè) POJO(Plain Ordinary Java Object)或者 String 對象構(gòu)造一個(gè)理論上不會(huì)有沖突的哈希函數(shù)彰阴,但是 hashCode() 函數(shù)的返回值是 int 型。根據(jù)鴿籠理論拍冠,當(dāng)我們的對象超過 232 個(gè)時(shí)尿这,這些對象會(huì)發(fā)生哈希沖突。

因此庆杜,實(shí)現(xiàn)HashMap的一個(gè)重要考量射众,就是盡可能地避免哈希沖突。HashMap在JDK 1.8中的做法是晃财,用鏈表紅黑樹存儲(chǔ)相同hash值的value叨橱。當(dāng)Hash沖突的個(gè)數(shù)比較少時(shí),使用鏈表断盛,否則使用紅黑樹罗洗。

底層實(shí)現(xiàn)

HashMap實(shí)現(xiàn)的接口如下:

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

HashMap繼承自抽象類AbstractMap,實(shí)現(xiàn)了Map接口钢猛,AbstractMap類實(shí)現(xiàn)了Map接口的部分方法伙菜,因此Map的最終實(shí)現(xiàn)類直接繼承AbstractMap,可以減少很多工作量命迈。

先來看HashMap內(nèi)部兩個(gè)重要的靜態(tài)內(nèi)部類贩绕。

單向鏈表的節(jié)點(diǎn)Node

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        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;
        }
    }

Node實(shí)現(xiàn)了Map的內(nèi)部接口Entry火的,Entry接口定義了鍵值對(key-value pair)的基本操作,Node類提供了這些方法的實(shí)現(xiàn)并且還含有一個(gè)next引用淑倾,作為單鏈表的實(shí)現(xiàn)用來指向下一個(gè)Node卫玖。

紅黑樹的節(jié)點(diǎn)TreeNode

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }

    /** * Returns root of tree containing this node. */
    final TreeNode<K,V> root() {
        for (TreeNode<K,V> r = this, p;;) {
            if ((p = r.parent) == null)
                return r;
            r = p;
        }
    }
    ……
}

當(dāng)一個(gè)單鏈表沖突的結(jié)點(diǎn)數(shù)超過預(yù)設(shè)值時(shí),將會(huì)把這個(gè)單鏈表自動(dòng)調(diào)整為紅黑樹踊淳。這樣做的好處是假瞬,最壞的情況下即所有的key都Hash沖突,采用鏈表的話查找時(shí)間為O(n),而采用紅黑樹為O(logn)迂尝。

HashMap的幾個(gè)重要字段如下:

    //存儲(chǔ)數(shù)據(jù)的Node數(shù)組脱茉,長度是2的冪。
    transient Node<K,V>[] table;
    //鍵值對緩存垄开,它們的映射關(guān)系集合保存在entrySet中琴许。即使Key在外部修改導(dǎo)致hashCode變化,緩存中還可以找到映射關(guān)系
    transient Set<Map.Entry<K,V>> entrySet;
    //map中保存的鍵值對的數(shù)量
    transient int size;
    //map結(jié)構(gòu)被改變的次數(shù)
    transient int modCount;
    //需要調(diào)整大小的極限值(容量*裝載因子)
    int threshold;
    //裝載因子溉躲,在后面會(huì)進(jìn)行詳細(xì)介紹
    final float loadFactor;

HashMap內(nèi)部使用Node數(shù)組實(shí)現(xiàn)了一個(gè)哈希桶數(shù)組table榜田。可以看出锻梳,HashMap還是憑借數(shù)組實(shí)現(xiàn)的箭券,數(shù)組的元素是單鏈表或紅黑樹,對于key的hash值相等的key-value pair疑枯,它們將分別作為一個(gè)結(jié)點(diǎn)(Node或TreeNode)存儲(chǔ)在同一個(gè)單鏈表或紅黑樹中辩块。

我們知道數(shù)組的特點(diǎn):尋址容易,插入和刪除困難荆永,而鏈表的特點(diǎn)是:尋址困難废亭,插入和刪除容易,紅黑樹則對插入時(shí)間具钥、刪除時(shí)間和查找時(shí)間提供了最好可能的最壞情況擔(dān)保豆村。HashpMap將這三者結(jié)合在一起。

HashMap的數(shù)據(jù)結(jié)構(gòu)如下圖所示:

img

此外骂删,這里的modCount屬性掌动,記錄了map結(jié)構(gòu)被改變的次數(shù),它與“fail-fast”機(jī)制的實(shí)現(xiàn)息息相關(guān)桃漾。fail-fast機(jī)制是Java集合的一種錯(cuò)誤檢測機(jī)制坏匪,假設(shè)存在兩個(gè)線程(線程1拟逮、線程2)撬统,線程1通過Iterator在遍歷集合A中的元素,在某個(gè)時(shí)候線程2修改了集合A的結(jié)構(gòu)(是結(jié)構(gòu)上面的修改敦迄,而不是簡單的修改集合元素的內(nèi)容)恋追,那么這個(gè)時(shí)候程序就會(huì)拋出 ConcurrentModificationException 異常凭迹,從而產(chǎn)生fail-fast機(jī)制。

對于HashMap內(nèi)容的修改都將使modCount的值增加苦囱,在迭代器初始化過程中會(huì)將這個(gè)值賦給迭代器的expectedModCount,在迭代過程中嗅绸,判斷modCount跟expectedModCount是否相等,如果不相等就表示已經(jīng)有其他線程修改了Map撕彤。

HashMap的一些重要的靜態(tài)全局變量如下鱼鸠,它們與HashMap規(guī)避哈希碰撞的策略息息相關(guān):

    /** * table默認(rèn)的初始容量,它的值必須是2的整數(shù)冪 */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /** * table的最大容量羹铅,必須小于2的30次方蚀狰,如果傳入的容量大于這個(gè)值,將被替換為該值 */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /** * 默認(rèn)裝載因子职员,如果在構(gòu)造函數(shù)中不顯式指定裝載因子麻蹋,則默認(rèn)使用該值。 */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /** * 結(jié)點(diǎn)沖突數(shù)達(dá)到8時(shí)焊切,就會(huì)對哈希表進(jìn)行調(diào)整扮授,如果table容量小于64,那么會(huì)進(jìn)行擴(kuò)容专肪, * 如果不小于64刹勃,那么會(huì)將沖突數(shù)達(dá)到8的那個(gè)單鏈表調(diào)整為紅黑樹. */
    static final int TREEIFY_THRESHOLD = 8;

    /** * 如果原先就是紅黑樹,resize以后沖突結(jié)點(diǎn)數(shù)少于6了嚎尤,就把紅黑色恢復(fù)成單鏈表 */
    static final int UNTREEIFY_THRESHOLD = 6;

    /** * 如果table的容量少于64深夯,那么即使沖突結(jié)點(diǎn)數(shù)達(dá)到TREEIFY_THRESHOLD后不會(huì)把該單鏈表調(diào)整成紅黑數(shù),而是將table擴(kuò)容 */
    static final int MIN_TREEIFY_CAPACITY = 64;

HashMap使用的hash算法如下:

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

使用hash值的高位16位與低16進(jìn)行XORs操作诺苹,算法簡潔有效咕晋。

常用API

看完了HashMap的基本數(shù)據(jù)結(jié)構(gòu)以后,來看一下常用方法的源碼收奔,首先自然想到的是get(key)put(key,value)掌呜。

get(key)

get(key)方法的作用是的源碼如下:

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

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

我們將要查找的key值傳給get,它調(diào)用hashCode計(jì)算hash從而得到bucket位置坪哄,并進(jìn)一步調(diào)用equals()方法確定鍵值對质蕉。取模算法中的除法運(yùn)算效率很低,在HashMap中通過h & (n-1)替代取模翩肌,得到所在數(shù)組位置模暗,效率會(huì)高很多(前提是保證數(shù)組的容量是2的整數(shù)倍)。

resize()

在介紹put方法之前還要先來看一下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;
        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;
        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;
                            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;
    }

當(dāng)HashMap中的元素個(gè)數(shù)超過 數(shù)組大小 * loadFactor 時(shí)兑宇,就會(huì)進(jìn)行數(shù)組擴(kuò)容,loadFactor的默認(rèn)值為0.75粱坤,這是一個(gè)折中的取值隶糕。也就是說瓷产,默認(rèn)情況下,數(shù)組大小為16枚驻,那么當(dāng)HashMap中元素個(gè)數(shù)超過 16 * 0.75=12 的時(shí)候濒旦,就把數(shù)組的大小擴(kuò)展為 2 * 16=32 ,即擴(kuò)大一倍再登,然后重新計(jì)算每個(gè)元素在數(shù)組中的位置尔邓,而這是一個(gè)非常消耗性能的操作,所以如果我們已經(jīng)預(yù)知HashMap中元素的個(gè)數(shù)锉矢,那么預(yù)設(shè)元素的個(gè)數(shù)能夠有效的提高HashMap的性能铃拇。

put(key,value)

put(key,value)方法的作用是向HashMap中添加一對key-value pair。源碼如下:

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

    /** * 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;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            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 {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        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;
                }
            }
            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;
    }

將key-value pair傳給put方法時(shí)沈撞,它調(diào)用hashCode計(jì)算hash從而得到bucket位置慷荔,進(jìn)而,HashMap根據(jù)當(dāng)前bucket的占用情況自動(dòng)調(diào)整容量(超過Load Factor則resize為原來的2倍)缠俺。

如果沒有發(fā)生碰撞就直接放到bucket里显晶,如果發(fā)生碰撞,Hashmap先通過鏈表將產(chǎn)生碰撞沖突的元素組織起來壹士,如果一個(gè)bucket中碰撞沖突的元素超過某個(gè)限制(默認(rèn)是8)磷雇,則使用紅黑樹來替換鏈表,從而提高速度躏救。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末唯笙,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子盒使,更是在濱河造成了極大的恐慌崩掘,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件少办,死亡現(xiàn)場離奇詭異苞慢,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)英妓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進(jìn)店門挽放,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蔓纠,你說我怎么就攤上這事辑畦。” “怎么了腿倚?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵纯出,是天一觀的道長。 經(jīng)常有香客問我,道長潦刃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任懈叹,我火速辦了婚禮乖杠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘澄成。我一直安慰自己胧洒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布墨状。 她就那樣靜靜地躺著卫漫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪肾砂。 梳的紋絲不亂的頭發(fā)上列赎,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天,我揣著相機(jī)與錄音镐确,去河邊找鬼包吝。 笑死,一個(gè)胖子當(dāng)著我的面吹牛源葫,可吹牛的內(nèi)容都是我干的诗越。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼息堂,長吁一口氣:“原來是場噩夢啊……” “哼嚷狞!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起荣堰,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤床未,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后振坚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體即硼,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年屡拨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了只酥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,926評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡呀狼,死狀恐怖裂允,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情哥艇,我是刑警寧澤绝编,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響十饥,放射性物質(zhì)發(fā)生泄漏窟勃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一逗堵、第九天 我趴在偏房一處隱蔽的房頂上張望秉氧。 院中可真熱鬧,春花似錦蜒秤、人聲如沸汁咏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽攘滩。三九已至,卻和暖如春纸泡,著一層夾襖步出監(jiān)牢的瞬間漂问,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工女揭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留级解,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓田绑,卻偏偏與公主長得像勤哗,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子掩驱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評論 2 354

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