集合框架 (第 07 篇) 源碼分析:jdk1.7版 HashMap

一笛求、集合框架源碼分析

原文持續(xù)更新鏈接: https://github.com/about-cloud/JavaCore

正文


本文是基于 jdk1.7.0_79 分析

一难述、繼承關(guān)系

HashMap1.7vExtend

二踩身、數(shù)據(jù)結(jié)構(gòu)

jdk1.7 HashMap的底層數(shù)據(jù)結(jié)構(gòu):數(shù)組 + 單向線性鏈表胀茵。

HashMap的元素就是在 Map.Entry 的基礎(chǔ)上實現(xiàn)的Entry項。

上一節(jié)分析了 哈希沖突 和 解決哈希沖突的算法挟阻,HashMap 就是基于 鏈表法 來解決哈希沖突的琼娘。

jdk1.7 HashMap圖片

重要屬性(請記住這些常量和變量的含義,下面源碼分析會用到)

/** 默認初始容量 16 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 既 16
/** 最大容量 10.7億+(這里也用到效率高的位運算) */
static final int MAXIMUM_CAPACITY = 1 << 30;
/** 默認負載因子 0.75 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/** 當(dāng) table 未填充的時候赁濒,共享這個空table */
static final Entry<?,?>[] EMPTY_TABLE = {};
/** (這個表又稱為基本表)該 table 根據(jù)需要而調(diào)整大小轨奄。長度必須始終是2的次冪。 */
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
/** map 中 存儲 key-value 映射的數(shù)量 */
transient int size;
/** 下次調(diào)整大小時的閾值(容量 * 負載因子) */
int threshold;
/** 哈希表的負載因子 */
final float loadFactor;
/** 此 HashMap 修改的次數(shù) */
transient int modCount;
/** 默認容量的最大值:Integer.MAX_VALUE */
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

HashMap 重點元素 項Entry<K, V>:之前文章已講解了 Map.Entry 接口拒炎,下面就來分析一下 jdk1.7 HashMap實現(xiàn)Map.EntryEntry<K, V>

static class Entry<K,V> implements Map.Entry<K,V> {
    // final 修飾的 key挪拟,防止被重復(fù)賦值
    final K key;
    // 可被重復(fù)設(shè)置值的value
    V value;
    // 此項的下一項(用于鏈表。并沒有類四的 Entry<K, V> prev击你,說名是單鏈表)
    Entry<K,V> next;
    int hash;

    /**
     * 構(gòu)造方法用于注入 entry項 的屬性值(或引用)
     * 參數(shù)從左至右依次是:key的哈希碼玉组,key,value丁侄,指向的下一個entry
     */
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    // getter & toString 方法
    public final K getKey() {
        return key;
    }
    public final V getValue() {
        return value;
    }
    public final String toString() {
        return getKey() + "=" + getValue();
    }
    
    // 設(shè)置節(jié)點的新值value
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    
    // 比較節(jié)點的方法
    public final boolean equals(Object o) {
        // 檢查類型
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        // 當(dāng)前項的key
        Object k1 = getKey();
        // 被比較對象的key
        Object k2 = e.getKey();
        // 這里說明一下 Object惯雳,它的 equals方法很簡單,就是用 == 來做判斷的
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }
    
    // 返回節(jié)點的哈希碼
    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }

    /**
     * 當(dāng)entry被訪問時鸿摇,都會調(diào)用此方法
     * 這里只不過是一個空方法
     */
    void recordAccess(HashMap<K,V> m) {
    }

    /**
     * 每當(dāng)entry項從表格中刪除時石景,都會調(diào)用這個空方法
     */
    void recordRemoval(HashMap<K,V> m) {
    }
}

三、添加元素項

public V put(K key, V value) {
    // 判斷是否為空表
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        // 如果key為null的情況下拙吉,將將鍵值對放在table[0]處
        // 如果table[0]已存在元素潮孽,則將替換value
        return putForNullKey(value);
    // key的哈希值
    int hash = hash(key);
    // 可以的哈希碼對表的長度模運算,計算并得到哈希槽的位置
    int i = indexFor(hash, table.length);
    // 對哈希桶(鏈表)進行遍歷筷黔,靠指針地址移動
    // 查找是否包含key的項往史,如果包含就將value換掉
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 該元素的哈希碼與新增項的key的哈希碼項相等,并且 key 也相同
        // 那么就會替換 value(因為key具有唯一性佛舱,相同的key要替換value)
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
           // 被替換的舊值
            V oldValue = e.value;
            // 選用新的value
            e.value = value;
            // 調(diào)用上面的空方法
            e.recordAccess(this);
            // 返回舊值
            return oldValue;
        }
    }

    // 如果上面for循環(huán)沒有查找到key的存在(或者說沒有找到相同的key)椎例,那么就新添加一項
    modCount++; // modCount加1
    // 添加entry項
    addEntry(hash, key, value, i);
    return null;
}

/**
 * 將具有指定key挨决、value、key的哈希碼订歪、桶號(也就是哈希槽的位置)的條目(元素)(項)
 * 添加到指定的桶(哈希桶)脖祈。
 * 此方法會在適當(dāng)?shù)那闆r下調(diào)整桶的大小
 * 子類也可以重寫此方法來改變put添加的行為
 */
void addEntry(int hash, K key, V value, int bucketIndex) {
    // 如果已添加元素項的實際大小達到了HashMap所承載的閾值,并且哈希槽的位置不為空
    // 那么就進行擴容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        // 擴容后的大小是2倍于原基本表的長度
        resize(2 * table.length);
        // 因為基本表已經(jīng)擴容刷晋,那么對key重新計算哈希值
        // 如果 key 不為 null 那么計算key的哈希值撒犀,否則哈希值直接記為0
        hash = (null != key) ? hash(key) : 0;
        // 根據(jù)哈希碼和基本表(數(shù)組)長度計算桶號
        // indexFor 方法并沒有使用模運行,而是使用高性能的邏輯與&運算
        bucketIndex = indexFor(hash, table.length);
    }
    
    createEntry(hash, key, value, bucketIndex);
}

創(chuàng)建元素項的方法:

/**
 * 此方法與 addEntry 不同掏秩,只有在創(chuàng)建 entry項的時候使用此方法
 * 作為構(gòu)建(或偽構(gòu)建) Map 的一部分或舞。"偽構(gòu)建" 是指 克隆、反序列蒙幻。
 * 使用此方法是不必擔(dān)心擴容問題映凳。
 * 子類可以重新此方法改變方法的功能,比如將單向鏈表改成雙向鏈表等等邮破。
 */
void createEntry(int hash, K key, V value, int bucketIndex) {
    // 原位置的元素項要下沉诈豌,新的元素放在哈希槽(桶頂)的位置
    Entry<K,V> e = table[bucketIndex];
    // 構(gòu)造新的元素項放在哈西槽(桶頂),同步把原有的元素項鏈入其后
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    // 實際大小加1
    size++;
}

鏈表的時候講要添加的元素項放到桶頂抒和,那么先進的元素項位于鏈表的尾部矫渔,后進的元素項位于鏈表的頭部。(這只是本版本 HashMap 的實現(xiàn)方式)

HashMap解決哈希沖突

擴容機制

/**
 * 擴容方法就是給 map 換一個更大容量的新數(shù)組摧莽。  
 * 前提前提條件是已添加元素項的實際大小達到了HashMap所承載的閾值庙洼。
 *
 * 如果當(dāng)前容量達到最大容量, 此方法也能給map擴容。但是可以設(shè)置閾值為Integer類型的最大值镊辕。
 *
 * @param newCapacity 新的容量, 必須是2的次冪;
 *        必須大于當(dāng)前容量油够,除非當(dāng)前容量達到了最大值。
 *        (在這種情況下值是無關(guān)緊要的)
 */
void resize(int newCapacity) {
    // 當(dāng)前table數(shù)組
    Entry[] oldTable = table;
    // 當(dāng)前table數(shù)組長度
    int oldCapacity = oldTable.length;
    // 如果當(dāng)前數(shù)組達到了最大容量(1<<30)征懈,那么賦值閾值為Integer的最大值石咬,并直接返回??
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    // new 新的數(shù)組
    Entry[] newTable = new Entry[newCapacity];
    // 將當(dāng)前的數(shù)組上索引元素項轉(zhuǎn)移到新的數(shù)組上
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    // 當(dāng)前數(shù)組指向新數(shù)組,完成擴容
    table = newTable;
    // 計算閾值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

/**
 * 將當(dāng)前的數(shù)組上索引元素項轉(zhuǎn)移到新的數(shù)組上
 */
void transfer(Entry[] newTable, boolean rehash) {
    // 新數(shù)組的長度(容量)
    int newCapacity = newTable.length;
    // 首先橫向遍歷數(shù)組
    for (Entry<K,V> e : table) {
        // 然后縱向遍歷鏈表
        while(null != e) {
            // 鏈表中提前獲取下一個元素項(節(jié)點)
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            // 重新計算存放元素
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

上面已經(jīng)看出每次擴容時都伴隨著所有元素項的重新 選址 存放卖哎,這不僅大大犧牲性能鬼悠,而且耗時。所以在使用 HashMap 一定要評估存放元素項的數(shù)量亏娜,指定map的大小焕窝。

四、刪除元素項

刪除元素項的的思路基本和添加操作相似照藻,只不過一個是添加袜啃,一個是刪除汗侵。先根據(jù) key 計算 哈希槽(桶的位置)幸缕,然后循環(huán)鏈表比較判斷群发,這里參考:telescope:之前關(guān)于 LinkedList 文章的刪除節(jié)點操作吧。

其他問題

為什么 HashMap 的容量必須是2的次冪呢发乔?

key的哈希碼對 基本表 做 邏輯與 運算 h&(length-1)熟妓,來確定此元素項的數(shù)組上的位置。 原因就出在 二進制與& 操作上了栏尚。

與& 運算規(guī)則:0&0=0; 0&1=0; 1&0=0; 1&1=1;

(指數(shù)形式)length (十進制)length (十進制)length - 1 (二進制)length - 1
1^2 2 1 1
2^2 4 3 11
3^2 8 7 111
4^2 16 15 1111
...^2 ... ... ...

這種情況下起愈,length - 1 二進制的 最右位 永遠是 1。這樣 0 & 1 = 0译仗,1 & 1 = 1 的結(jié)果既有 0 又有1抬虽。對于 哈希槽 的二進制最右位為 1 (十進制的奇數(shù))的位置就有可能被填充,而不至于浪費存儲空間纵菌。

如果 HashMap 的容量不是 2 的次冪阐污,比如 容量length為 19length - 1 的二進制為 10010咱圆,任何常數(shù)與之作 與& 運算笛辟,二進制最右位都是 0,那么只能存放 (十進制)尾數(shù)為 偶數(shù) 的數(shù)組位置序苏,這樣會大大浪費空間手幢。

原文持續(xù)更新鏈接: https://github.com/about-cloud/JavaCore

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市忱详,隨后出現(xiàn)的幾起案子围来,更是在濱河造成了極大的恐慌,老刑警劉巖匈睁,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件管钳,死亡現(xiàn)場離奇詭異,居然都是意外死亡软舌,警方通過查閱死者的電腦和手機才漆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來佛点,“玉大人醇滥,你說我怎么就攤上這事〕” “怎么了鸳玩?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長演闭。 經(jīng)常有香客問我不跟,道長,這世上最難降的妖魔是什么米碰? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任窝革,我火速辦了婚禮购城,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘虐译。我一直安慰自己瘪板,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布漆诽。 她就那樣靜靜地躺著侮攀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪厢拭。 梳的紋絲不亂的頭發(fā)上兰英,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天,我揣著相機與錄音供鸠,去河邊找鬼箭昵。 笑死,一個胖子當(dāng)著我的面吹牛回季,可吹牛的內(nèi)容都是我干的家制。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼泡一,長吁一口氣:“原來是場噩夢啊……” “哼颤殴!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鼻忠,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤涵但,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后帖蔓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體矮瘟,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年塑娇,在試婚紗的時候發(fā)現(xiàn)自己被綠了澈侠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡埋酬,死狀恐怖哨啃,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情写妥,我是刑警寧澤拳球,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站珍特,受9級特大地震影響祝峻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一莱找、第九天 我趴在偏房一處隱蔽的房頂上張望酬姆。 院中可真熱鬧,春花似錦宋距、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至诱篷,卻和暖如春壶唤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背棕所。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工闸盔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人琳省。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓迎吵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親针贬。 傳聞我的和親對象是個殘疾皇子击费,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,614評論 2 353

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