白話HashMap源碼(上)

HashMap一句話就可以說個大概:

用哈希算法把key計算出索引index丈攒,然后將key芦瘾、value構(gòu)成的HashMapEntry放入HashMapEntry[index]际度,即完成了put功能首懈,get時將key重計算出index去取HashMapEntry菱农。

以上只是最表層的思想炕淮,如果不同key計算出相同的index呢?HashMapEntry里的next就起到作用了干毅。

HashMap

如上圖:橫排的Entry為HashMapEntry[]宜猜,縱向的箭頭表示同一index下存在多個Entry,Entry之間采用的是鏈?zhǔn)酱鎯Α?/p>

HashMap的大體設(shè)計思路就是如此了硝逢,接下來看看源碼是如何寫的(源碼是android-23)姨拥。

構(gòu)造函數(shù)

無參構(gòu)造函數(shù):創(chuàng)建空表設(shè)置閾值

    /**
     * Min capacity (other than zero) for a HashMap. Must be a power of two
     * greater than 1 (and less than 1 << 30).
     */
    //HashMap最小的容量。此容量必須是以2為底的次方數(shù)渠鸽,且大于1小于2的30次方
    private static final int MINIMUM_CAPACITY = 4;

    /**
     * An empty table shared by all zero-capacity maps (typically from default
     * constructor). It is never written to, and replaced on first put. Its size
     * is set to half the minimum, so that the first resize will create a
     * minimum-sized table.
     */
    //空表叫乌,容量是最小容量的一半
    private static final Entry[] EMPTY_TABLE
            = new HashMapEntry[MINIMUM_CAPACITY >>> 1];//無符號右移

    public HashMap() {
        table = (HashMapEntry<K, V>[]) EMPTY_TABLE;
        threshold = -1; // Forces first put invocation to replace EMPTY_TABLE閾值超過閾值時需要擴(kuò)容
    }

帶容量的構(gòu)造參數(shù)

對傳入的容量值稍作處理,然后創(chuàng)建表

    /**
     * Max capacity for a HashMap. Must be a power of two >= MINIMUM_CAPACITY.
     */
    //吧啦吧啦徽缚,容量必須是以2為底的次方數(shù)憨奸,且大于MINIMUM_CAPACITY
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    public HashMap(int capacity) {
        if (capacity < 0) {//拋異常
            throw new IllegalArgumentException("Capacity: " + capacity);
        }

        if (capacity == 0) {//容量為0時和無參構(gòu)造函數(shù)邏輯一致,我認(rèn)為可以HashMap()然后return搞定凿试,不需要重復(fù)代碼
            @SuppressWarnings("unchecked")
            HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE;
            table = tab;
            threshold = -1; // Forces first put() to replace EMPTY_TABLE
            return;
        }

        if (capacity < MINIMUM_CAPACITY) {
            capacity = MINIMUM_CAPACITY;
        } else if (capacity > MAXIMUM_CAPACITY) {
            capacity = MAXIMUM_CAPACITY;
        } else {
            capacity = Collections.roundUpToPowerOfTwo(capacity);//獲取大于等于capacity并且是 2 的次方的整數(shù)
        }
        makeTable(capacity);
    }

    private HashMapEntry<K, V>[] makeTable(int newCapacity) {
        @SuppressWarnings("unchecked") HashMapEntry<K, V>[] newTable
                = (HashMapEntry<K, V>[]) new HashMapEntry[newCapacity];//根據(jù)傳入的容量創(chuàng)建數(shù)組
        table = newTable;
        threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity
//設(shè)置容量閾值排宰,如果大于閾值則擴(kuò)充數(shù)組大小
        return newTable;
    }

帶負(fù)載因子的構(gòu)造方法

什么是負(fù)載因子?

HashMap的存儲結(jié)構(gòu)是一個數(shù)組那婉,數(shù)組內(nèi)的項為鏈表板甘。

數(shù)組的優(yōu)勢是查找迅速,由于分配的內(nèi)存空間連續(xù)详炬,但產(chǎn)生的index不一定連續(xù)盐类,所以會產(chǎn)生空間的浪費(fèi)。

鏈表的優(yōu)勢是增刪快速,但查找速度不如數(shù)組在跳,必須一個一個的向下查找枪萄。

負(fù)載因子的功能就是為了協(xié)調(diào)數(shù)組與鏈表之間的優(yōu)劣,負(fù)載因子大則鏈表的長度會大以致查找速度會降低硬毕,否則數(shù)組的長度大造成空間的浪費(fèi)呻引。

根據(jù)實(shí)際的使用情況礼仗,設(shè)置合適的負(fù)載因子吐咳。由于源碼并未實(shí)際設(shè)置,所以負(fù)載因子的功能只帶過元践。

    /**
     * Constructs a new {@code HashMap} instance with the specified capacity and
     * load factor.
     *
     * @param capacity
     *            the initial capacity of this hash map.
     * @param loadFactor
     *            the initial load factor.
     * @throws IllegalArgumentException
     *                when the capacity is less than zero or the load factor is
     *                less or equal to zero or NaN.
     */
    public HashMap(int capacity, float loadFactor) {
        this(capacity);

        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new IllegalArgumentException("Load factor: " + loadFactor);
        }

        /*
         * Note that this implementation ignores loadFactor; it always uses
         * a load factor of 3/4. This simplifies the code and generally
         * improves performance.
         */
    }

帶子集的構(gòu)造方法

根據(jù)子集的大小調(diào)整自身的數(shù)組大小韭脊,將子集數(shù)據(jù)裝填到自身。

    public HashMap(Map<? extends K, ? extends V> map) {
        //設(shè)置數(shù)組大小
        this(capacityForInitSize(map.size()));
        //裝填子集
        constructorPutAll(map);
    }

    //返回需要的大小
    static int capacityForInitSize(int size) {
        int result = (size >> 1) + size; // Multiply by 3/2 to allow for growth 
//對傳入的size乘以1.5单旁,但是移位的操作快速所以采用了移位代替乘

        // boolean expr is equivalent to result >= 0 && result<MAXIMUM_CAPACITY
//返回值要求>= 0 且<MAXIMUM_CAPACITY沪羔,具體如何實(shí)現(xiàn)
//假設(shè)MAXIMUM_CAPACITY為1000
//(MAXIMUM_CAPACITY-1)=0111
//~(MAXIMUM_CAPACITY-1)=1000
//所以result & ~(MAXIMUM_CAPACITY-1))的目的是去低位,與0一定為0象浑,除非高位不為0蔫饰,但高位不為0的話就大于MAXIMUM_CAPACITY了所以就有了如下
        return (result & ~(MAXIMUM_CAPACITY-1))==0 ? result : MAXIMUM_CAPACITY;
    }

    //裝填
    final void constructorPutAll(Map<? extends K, ? extends V> map) {
        if (table == EMPTY_TABLE) {//如果是空表則翻倍空間大小
            doubleCapacity(); // Don't do unchecked puts to a shared table.
        }
        for (Entry<? extends K, ? extends V> e : map.entrySet()) {
//組裝key value 進(jìn)行裝填
            constructorPut(e.getKey(), e.getValue());
        }
    }

裝填項

    private void constructorPut(K key, V value) {
        if (key == null) {//key為null時維護(hù)entryForNullKey 
            HashMapEntry<K, V> entry = entryForNullKey;
            if (entry == null) {
                entryForNullKey = constructorNewEntry(null, value, 0, null);
                size++;
            } else {
                entry.value = value;
            }
            return;
        }

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        int index = hash & (tab.length - 1);//計算出index
        HashMapEntry<K, V> first = tab[index];//此項不為空向下搜索鏈表
        for (HashMapEntry<K, V> e = first; e != null; e = e.next) {
            if (e.hash == hash && key.equals(e.key)) {
                e.value = value;
                return;
            }
        }

        // No entry for (non-null) key is present; create one
        tab[index] = constructorNewEntry(key, value, hash, first);
        size++;
    }

    HashMapEntry<K, V> constructorNewEntry(
            K key, V value, int hash, HashMapEntry<K, V> first) {
        return new HashMapEntry<K, V>(key, value, hash, first);
    }

PUT

public V put(K key, V value)
傳入key、value愉豺,計算hash
如果存在key則更新并返回舊值篓吁,否則添加新HashMapEntry并返回null

除此以外有一個特例,HashMap內(nèi)還維護(hù)了一個entryForNullKey用于存儲key=null時的value

    @Override public V put(K key, V value) {
        if (key == null) {
            return putValueForNullKey(value);
        }

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        int index = hash & (tab.length - 1);
        for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
            if (e.hash == hash && key.equals(e.key)) {
                preModify(e);
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }

        // No entry for (non-null) key is present; create one
        modCount++;
        if (size++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }
        addNewEntry(key, value, hash, index);
        return null;
    }

第一步存null的Key

    transient int size;
    transient int modCount;
    //transient 此關(guān)鍵字不參與序列化蚪拦,存儲時不會保存杖剪,只存于此對象。

    private V putValueForNullKey(V value) {
        HashMapEntry<K, V> entry = entryForNullKey;
        if (entry == null) {//不存在舊值
            addNewEntryForNullKey(value);
            size++;//總存儲量
            modCount++;//修改次數(shù)
            return null;
        } else {//存在舊值
            preModify(entry);//修改前操作
            V oldValue = entry.value;
            entry.value = value;
            return oldValue;
        }
    }

    void addNewEntryForNullKey(V value) {
        entryForNullKey = new HashMapEntry<K, V>(null, value, 0, null);
    }

    //空實(shí)現(xiàn)驰贷,可以做一些修改前的預(yù)處理
    void preModify(HashMapEntry<K, V> e) { }

第二步更新key

        HashMapEntry<K, V>[] tab = table;
        //由hash求得index
        int index = hash & (tab.length - 1);
        //遍歷鏈表
        for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
            if (e.hash == hash && key.equals(e.key)) {//key存在于HashMap中的條件:hash和key相同
                preModify(e);//預(yù)處理
                V oldValue = e.value;
                e.value = value;//更新
                return oldValue;
            }
        }

第三步存新key

能夠走到這里說明key不為null盛嘿,且之前沒有存儲過此key

        modCount++;//修改計數(shù)加一
        if (size++ > threshold) {//存儲空間大于閾值,加倍空間
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }

        addNewEntry(key, value, hash, index);
        return null;

增加空間大小

    private HashMapEntry<K, V>[] doubleCapacity() {
        HashMapEntry<K, V>[] oldTable = table;
        int oldCapacity = oldTable.length;
        //如果已經(jīng)是最大的則無法增加
        if (oldCapacity == MAXIMUM_CAPACITY) {
            return oldTable;
        }
        //容量翻倍
        int newCapacity = oldCapacity * 2;
        HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
        //表中無數(shù)據(jù)直接返回
        if (size == 0) {
            return newTable;
        }
        //表中有數(shù)據(jù)括袒,需要將數(shù)據(jù)轉(zhuǎn)移到新表
        for (int j = 0; j < oldCapacity; j++) {
            //代碼見下
        }
        return newTable;
    }

將數(shù)據(jù)轉(zhuǎn)移到新表

疑問:在對鏈表重新分布處理時次兆,如果鏈表內(nèi)的entry被放入新index,但源碼并未對entry前項的next指針賦null锹锰。索引使用時并不會有問題类垦,但同一對象指針被存在了兩處。沒看懂望高人指點(diǎn)城须。

        for (int j = 0; j < oldCapacity; j++) {
            /*
             * Rehash the bucket using the minimum number of field writes.
             * This is the most subtle and delicate code in the class.
             */
            HashMapEntry<K, V> e = oldTable[j];
            if (e == null) {//null不管
                continue;
            }
            int highBit = e.hash & oldCapacity;//舊index
            HashMapEntry<K, V> broken = null;
            newTable[j | highBit] = e;//存入新的index
            //對鏈表重新分布蚤认,疑問處
            for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
                int nextHighBit = n.hash & oldCapacity;
                if (nextHighBit != highBit) {
                    if (broken == null)
                        newTable[j | nextHighBit] = n;
                    else
                        broken.next = n;
                    broken = e;
                    highBit = nextHighBit;
                }
            }
            if (broken != null)
                broken.next = null;
        }

新表處理過后進(jìn)行put

    void addNewEntry(K key, V value, int hash, int index) {
        table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
    }

GET

key為null的處理,否則由key計算hash找出index的項糕伐,對列表遍歷查找砰琢。

列表內(nèi)查找條件:
key地址相同

entry的hash相同且key的值相同

找不到返null

    public V get(Object key) {
        if (key == null) {
            HashMapEntry<K, V> e = entryForNullKey;
            return e == null ? null : e.value;
        }

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
                e != null; e = e.next) {
            K eKey = e.key;
            if (eKey == key || (e.hash == hash && key.equals(eKey))) {
                return e.value;
            }
        }
        return null;
    }

Remove

    @Override public V remove(Object key) {
        if (key == null) {//key為空的情況
            return removeNullKey();
        }
        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        int index = hash & (tab.length - 1);
        for (HashMapEntry<K, V> e = tab[index], prev = null;
                e != null; prev = e, e = e.next) {//鏈表的移除方式
            if (e.hash == hash && key.equals(e.key)) {
                if (prev == null) {
                    tab[index] = e.next;
                } else {
                    prev.next = e.next;
                }
                modCount++;
                size--;
                postRemove(e);
                return e.value;
            }
        }
    private V removeNullKey() {
        HashMapEntry<K, V> e = entryForNullKey;
        if (e == null) {
            return null;
        }
        entryForNullKey = null;
        modCount++;
        size--;
        postRemove(e);
        return e.value;
    }

    /**
     * Subclass overrides this method to unlink entry.
     */
//空實(shí)現(xiàn) 移除后操作
    void postRemove(HashMapEntry<K, V> e) { }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子陪汽,更是在濱河造成了極大的恐慌训唱,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挚冤,死亡現(xiàn)場離奇詭異况增,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)训挡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門澳骤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人澜薄,你說我怎么就攤上這事为肮。” “怎么了肤京?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵颊艳,是天一觀的道長。 經(jīng)常有香客問我忘分,道長棋枕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任妒峦,我火速辦了婚禮重斑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘舟山。我一直安慰自己绸狐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布累盗。 她就那樣靜靜地躺著寒矿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪若债。 梳的紋絲不亂的頭發(fā)上符相,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機(jī)與錄音蠢琳,去河邊找鬼啊终。 笑死,一個胖子當(dāng)著我的面吹牛傲须,可吹牛的內(nèi)容都是我干的蓝牲。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼泰讽,長吁一口氣:“原來是場噩夢啊……” “哼例衍!你這毒婦竟也來了昔期?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤佛玄,失蹤者是張志新(化名)和其女友劉穎硼一,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體梦抢,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡般贼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了奥吩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哼蛆。...
    茶點(diǎn)故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖圈驼,靈堂內(nèi)的尸體忽然破棺而出人芽,到底是詐尸還是另有隱情望几,我是刑警寧澤绩脆,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站橄抹,受9級特大地震影響靴迫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜楼誓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一玉锌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧疟羹,春花似錦主守、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至愧杯,卻和暖如春涎才,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背力九。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工耍铜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人跌前。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓棕兼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親抵乓。 傳聞我的和親對象是個殘疾皇子伴挚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評論 2 355