HashMap 源碼分析

HashMap 是 Java 面試必考的知識(shí)點(diǎn),面試官?gòu)倪@個(gè)小知識(shí)點(diǎn)就可以了解我們對(duì) Java 基礎(chǔ)的掌握程度映企。網(wǎng)上的源碼分析總結(jié)太多太多了,現(xiàn)飯炒了三遍也還是要吃的,所以我在這里做一些整理和總結(jié)分享給大家要拂,也便于自己為明年實(shí)習(xí)生面試做準(zhǔn)備。

HashMap 簡(jiǎn)介

一句話概括之:HashMap 是一個(gè)散列表站楚,它存儲(chǔ)的內(nèi)容是鍵值對(duì) (key-value) 映射脱惰。

它根據(jù)鍵的 hashCode 值存儲(chǔ)數(shù)據(jù),大多數(shù)情況下可以直接定位到它的值窿春,因而具有很快的訪問(wèn)速度拉一,但遍歷順序卻是不確定的。

HashMap 最多只允許一條記錄的鍵為 null旧乞,允許多條記錄的值為 null蔚润。

HashMap 使用 hash 算法進(jìn)行數(shù)據(jù)的存儲(chǔ)和查詢(xún)。
內(nèi)部使用一個(gè) Entry 表示鍵值對(duì) key-value尺栖。
用 Entry 的數(shù)組保存所有鍵值對(duì), Entry 通過(guò)鏈表的方式鏈接后續(xù)的節(jié)點(diǎn) (1.8 后會(huì)根據(jù)鏈表長(zhǎng)度決定是否轉(zhuǎn)換成一棵樹(shù)類(lèi)似 TreeMap 來(lái)節(jié)省查詢(xún)時(shí)間) Entry 通過(guò)計(jì)算 key 的 hash 值來(lái)決定映射到具體的哪個(gè)數(shù)組(也叫 Bucket) 中嫡纠。

HashMap 非線程安全,即任一時(shí)刻可以有多個(gè)線程同時(shí)寫(xiě) HashMap延赌,可能會(huì)導(dǎo)致數(shù)據(jù)的不一致除盏。如果需要滿(mǎn)足線程安全,可以用 Collections 的 synchronizedMap 方法使 HashMap 具有線程安全的能力挫以,或者使用 ConcurrentHashMap者蠕。

HashMap 特性

  • Hash 相關(guān)的數(shù)據(jù)結(jié)構(gòu)本質(zhì)上都是 key value pair
  • Hash 中不能存在 duplicate key
  • HashMap 提供非常快速查找時(shí)間復(fù)雜度
  • HashMap 具體實(shí)現(xiàn)中掐松,null 可以作為 key 或者 value 存在
  • HashMap 不是線程安全

HashMap 實(shí)現(xiàn)

HashMap 1.7 版本

java 集合框架 08——HashMap 和源碼分析

HashMap 1.8 版本

存儲(chǔ)結(jié)構(gòu)

從結(jié)構(gòu)實(shí)現(xiàn)來(lái)講蠢棱,HashMap 是數(shù)組 + 鏈表 + 紅黑樹(shù)(JDK1.8 增加了紅黑樹(shù)部分)實(shí)現(xiàn)的锌杀。

hashMap內(nèi)存結(jié)構(gòu)圖.png

HashMap 常量定義:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    private static final long serialVersionUID = 362498820763181265L;
    /**
     * HashMap 的默認(rèn)初始容量為 16,必須為 2 的 n 次方 (一定是合數(shù))
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

    /**
     * HashMap 的最大容量為 2 的 30 次冪
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;        

    /**
     *  HashMap 的默認(rèn)負(fù)載因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 鏈表轉(zhuǎn)成紅黑樹(shù)的閾值泻仙。即在哈希表擴(kuò)容時(shí)糕再,當(dāng)鏈表的長(zhǎng)度(桶中元素個(gè)數(shù))超過(guò)這個(gè)值的時(shí)候,進(jìn)行鏈表到紅黑樹(shù)的轉(zhuǎn)變
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 紅黑樹(shù)轉(zhuǎn)為鏈表的閾值玉转。即在哈希表擴(kuò)容時(shí)突想,如果發(fā)現(xiàn)鏈表長(zhǎng)度(桶中元素個(gè)數(shù))小于 6,則會(huì)由紅黑樹(shù)重新退化為鏈表
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * HashMap 的最小樹(shù)形化容量究抓。這個(gè)值的意義是:位桶(bin)處的數(shù)據(jù)要采用紅黑樹(shù)結(jié)構(gòu)進(jìn)行存儲(chǔ)時(shí)猾担,整個(gè)Table的最小容量(存儲(chǔ)方式由鏈表轉(zhuǎn)成紅黑樹(shù)的容量的最小閾值)
     * 當(dāng)哈希表中的容量大于這個(gè)值時(shí),表中的桶才能進(jìn)行樹(shù)形化刺下,否則桶內(nèi)元素太多時(shí)會(huì)擴(kuò)容绑嘹,而不是樹(shù)形化
     * 為了避免進(jìn)行擴(kuò)容、樹(shù)形化選擇的沖突橘茉,這個(gè)值不能小于 4 * TREEIFY_THRESHOLD
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /**
     * Node 是 HashMap 的一個(gè)內(nèi)部類(lèi)工腋,實(shí)現(xiàn)了 Map.Entry 接口,本質(zhì)是就是一個(gè)映射 (鍵值對(duì))
     * Basic hash bin node, used for most entries.
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; // 用來(lái)定位數(shù)組索引位置
        final K key;
        V value;
        Node<K,V> next; // 鏈表的下一個(gè)node

        Node(int hash, K key, V value, Node<K,V> next) { ... }

        public final K getKey()        { ... }
        public final V getValue()      { ... }
        public final String toString() { ... }
        public final int hashCode() { ... }
        public final V setValue(V newValue) { ... }
        public final boolean equals(Object o) { ... }
    }

    /**
     * 哈希桶數(shù)組畅卓,分配的時(shí)候擅腰,table的長(zhǎng)度總是2的冪
     */
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * HashMap 中實(shí)際存儲(chǔ)的 key-value 鍵值對(duì)數(shù)量
     */
    transient int size;

    /**
     * 用來(lái)記錄 HashMap 內(nèi)部結(jié)構(gòu)發(fā)生變化的次數(shù),主要用于迭代的快速失敗機(jī)制
     */
    transient int modCount;

    /**
     * HashMap 的門(mén)限閥值/擴(kuò)容閾值翁潘,所能容納的 key-value 鍵值對(duì)極限趁冈,當(dāng)size>=threshold時(shí),就會(huì)擴(kuò)容
     * 計(jì)算方法:容量capacity * 負(fù)載因子load factor    
     */
    int threshold;

    /**
     * HashMap 的負(fù)載因子
     */
    final float loadFactor;
}

Node[] table 的初始化長(zhǎng)度 length(默認(rèn)值是 16)拜马,loadFactor 為負(fù)載因子 (默認(rèn)值 DEFAULT_LOAD_FACTOR 是 0.75)渗勘,threshold 是 HashMap 所能容納的最大數(shù)據(jù)量的 Node(鍵值對(duì)) 個(gè)數(shù)。

threshold = length * loadFactor俩莽。也就是說(shuō)旺坠,在數(shù)組定義好長(zhǎng)度之后,負(fù)載因子越大豹绪,所能容納的鍵值對(duì)個(gè)數(shù)越多价淌。
這里我們需要加載因子 (load_factor),加載因子默認(rèn)為 0.75瞒津,當(dāng) HashMap 中存儲(chǔ)的元素的數(shù)量大于 (容量 × 加載因子)蝉衣,也就是默認(rèn)大于 16*0.75=12 時(shí),HashMap 會(huì)進(jìn)行擴(kuò)容的操作巷蚪。

size 這個(gè)字段其實(shí)很好理解病毡,就是 HashMap 中實(shí)際存在的鍵值對(duì)數(shù)量。注意和 table 的長(zhǎng)度 length屁柏、容納最大鍵值對(duì)數(shù)量 threshold 的區(qū)別啦膜。而 modCount 字段主要用來(lái)記錄 HashMap 內(nèi)部結(jié)構(gòu)發(fā)生變化的次數(shù)有送,主要用于迭代的快速失敗。強(qiáng)調(diào)一點(diǎn)僧家,內(nèi)部結(jié)構(gòu)發(fā)生變化指的是結(jié)構(gòu)發(fā)生變化雀摘,例如 put 新鍵值對(duì),但是某個(gè) key 對(duì)應(yīng)的 value 值被覆蓋不屬于結(jié)構(gòu)變化八拱。

在 HashMap 中阵赠,哈希桶數(shù)組 table 的長(zhǎng)度 length 大小必須為 2 的 n 次方 (一定是合數(shù)),這是一種非常規(guī)的設(shè)計(jì)肌稻,常規(guī)的設(shè)計(jì)是把桶的大小設(shè)計(jì)為素?cái)?shù)清蚀。相對(duì)來(lái)說(shuō)素?cái)?shù)導(dǎo)致沖突的概率要小于合數(shù),具體證明可以參考 http://blog.csdn.net/liuqiyao_01/article/details/14475159爹谭,Hashtable 初始化桶大小為 11枷邪,就是桶大小設(shè)計(jì)為素?cái)?shù)的應(yīng)用(Hashtable 擴(kuò)容后不能保證還是素?cái)?shù))。HashMap 采用這種非常規(guī)設(shè)計(jì)诺凡,主要是為了在取模和擴(kuò)容時(shí)做優(yōu)化东揣,同時(shí)為了減少?zèng)_突,HashMap 定位哈希桶索引位置時(shí)绑洛,也加入了高位參與運(yùn)算的過(guò)程救斑。

這里存在一個(gè)問(wèn)題童本,即使負(fù)載因子和 Hash 算法設(shè)計(jì)的再合理真屯,也免不了會(huì)出現(xiàn)拉鏈過(guò)長(zhǎng)的情況,一旦出現(xiàn)拉鏈過(guò)長(zhǎng)穷娱,則會(huì)嚴(yán)重影響 HashMap 的性能绑蔫。于是,在 JDK1.8 版本中泵额,對(duì)數(shù)據(jù)結(jié)構(gòu)做了進(jìn)一步的優(yōu)化配深,引入了紅黑樹(shù)。而當(dāng)鏈表長(zhǎng)度太長(zhǎng)(默認(rèn)超過(guò) 8)時(shí)嫁盲,鏈表就轉(zhuǎn)換為紅黑樹(shù)篓叶,利用紅黑樹(shù)快速增刪改查的特點(diǎn)提高 HashMap 的性能,其中會(huì)用到紅黑樹(shù)的插入羞秤、刪除缸托、查找等算法。本文不再對(duì)紅黑樹(shù)展開(kāi)討論瘾蛋,想了解更多紅黑樹(shù)數(shù)據(jù)結(jié)構(gòu)的工作原理可以參考:http://blog.csdn.net/v_july_v/article/details/6105630

功能實(shí)現(xiàn)

HashMap 的內(nèi)部功能實(shí)現(xiàn)很多,本文主要從根據(jù) key 獲取哈希桶數(shù)組索引位置凭戴、put 方法的詳細(xì)執(zhí)行、擴(kuò)容過(guò)程等具有代表性的點(diǎn)深入展開(kāi)講解叼风。

解決 Hash 的的沖突的hash()方法

HashMap 的 hash 計(jì)算時(shí)先計(jì)算 hashCode(), 然后進(jìn)行二次 hash。

// 計(jì)算二次Hash
int hash = hash(key.hashCode());

// 通過(guò)Hash找數(shù)組索引
int i = hash & (tab.length-1);

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

這個(gè)方法非常巧妙棍苹,它總是通過(guò) h &(table.length -1) 來(lái)得到該對(duì)象的保存位置无宿,而 HashMap 底層數(shù)組的長(zhǎng)度總是 2 的 n 次方。

當(dāng) length 總是 2 的倍數(shù)時(shí)枢里,h & (length-1) 將是一個(gè)非常巧妙的設(shè)計(jì):
假設(shè) h=5,length=16, 那么 h & length - 1 將得到 5懈贺;
如果 h=6,length=16, 那么 h & length - 1 將得到 6
如果 h=15,length=16, 那么 h & length - 1 將得到 15;
但是當(dāng) h=16 時(shí) , length=16 時(shí)坡垫,那么 h & length - 1 將得到 0 了梭灿;
當(dāng) h=17 時(shí) , length=16 時(shí),那么 h & length - 1 將得到 1 了冰悠。

這樣保證計(jì)算得到的索引值總是位于 table 數(shù)組的索引之內(nèi)堡妒。

put()方法

視頻講解:How HashMap works in Java? With Animation!! whats new in java8 tutorial

put()方法大致的思路為:

  1. 對(duì) key 的 hashCode() 做 hash,然后再計(jì)算 index;
  2. 如果沒(méi)碰撞直接放到 bucket 里溉卓;
  3. 如果碰撞了皮迟,以鏈表的形式存在 buckets 后;
  4. 如果碰撞導(dǎo)致鏈表過(guò)長(zhǎng) (大于等于 TREEIFY_THRESHOLD=8)桑寨,就把鏈表轉(zhuǎn)換成紅黑樹(shù)伏尼;
  5. 如果節(jié)點(diǎn)已經(jīng)存在就替換 old value(保證 key 的唯一性)
  6. 如果 bucket 滿(mǎn)了 (超過(guò) load factor*current capacity),就要 resize尉尾。

具體步驟為:

  1. 如果 table 沒(méi)有使用過(guò)的情況(tab=table)==null || (n=tab.length) == 0爆阶,則以默認(rèn)大小進(jìn)行一次 resize
  2. 計(jì)算 key 的 hash 值,然后獲取底層 table 數(shù)組的第 (n-1)&hash 的位置的數(shù)組索引 tab[i] 處的數(shù)據(jù)沙咏,即 hash 對(duì) n 取模的位置辨图,依賴(lài)的是 n 為 2 的次方這一條件
  3. 先檢查該 bucket 第一個(gè)元素是否是和插入的 key 相等 (如果是同一個(gè)對(duì)象則肯定 equals)
  4. 如果不相等并且是 TreeNode 的情況,調(diào)用 TreeNode 的 put 方法
  5. 否則循環(huán)遍歷鏈表肢藐,如果找到相等的 key 跳出循環(huán)否則達(dá)到最后一個(gè)節(jié)點(diǎn)時(shí)將新的節(jié)點(diǎn)添加到鏈表最后, 當(dāng)前面找到了相同的 key 的情況下替換這個(gè)節(jié)點(diǎn)的 value 為新的 value故河。
  6. 最后如果新增了 key-value 對(duì),則增加 size 并且判斷是否超過(guò)了 threshold, 如果超過(guò)則需要進(jìn)行 resize 擴(kuò)容
put(K key, V val);
index = hash(key) & (n-1)

V val = get(Object key);
index = hash(key) & (n-1)
比較hashCode...
public V put(K key, V value) {
    // 對(duì)key的hashCode()做hash
    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;
    // table為空或者length=0時(shí)吆豹,以默認(rèn)大小擴(kuò)容鱼的,n為table的長(zhǎng)度    
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 計(jì)算index,并對(duì)null做處理痘煤,table[i]==null
    if ((p = tab[i = (n - 1) & hash]) == null)
        // (n-1)&hash 與Java7中indexFor方法的實(shí)現(xiàn)相同凑阶,若i位置上的值為空,則新建一個(gè)Node速勇,table[i]指向該Node晌砾。
        // 直接插入
        tab[i] = newNode(hash, key, value, null);
    else {
        // 若i位置上的值不為空,判斷當(dāng)前位置上的Node p 是否與要插入的key的hash和key相同
        Node<K,V> e; K k;
        // 若節(jié)點(diǎn)key存在烦磁,直接覆蓋value
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 判斷table[i]該鏈?zhǔn)欠袷羌t黑樹(shù)养匈,如果是紅黑樹(shù)哼勇,則直接在樹(shù)中插入鍵值對(duì)
        else if (p instanceof TreeNode)
            // 不同,且當(dāng)前位置上的的node p已經(jīng)是TreeNode的實(shí)例呕乎,則再該樹(shù)上插入新的node
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // table[i]該鏈?zhǔn)瞧胀ㄦ湵砘#M(jìn)行鏈表的插入操作
        else {
            // 在i位置上的鏈表中找到p.next為null的位置,binCount計(jì)算出當(dāng)前鏈表的長(zhǎng)度猬仁,如果繼續(xù)將沖突的節(jié)點(diǎn)插入到該鏈表中帝璧,會(huì)使鏈表的長(zhǎng)度大于tree化的閾值,則將鏈表轉(zhuǎn)換成tree湿刽。
            for (int binCount = 0; ; ++binCount) {
                // 如果遍歷到了最后一個(gè)節(jié)點(diǎn)的烁,說(shuō)明沒(méi)有匹配的key,則創(chuàng)建一個(gè)新的節(jié)點(diǎn)并添加到最后
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 鏈表長(zhǎng)度大于8轉(zhuǎn)換為紅黑樹(shù)進(jìn)行處理
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 遍歷過(guò)程中若發(fā)現(xiàn) key 已經(jīng)存在直接覆蓋 value 并跳出循環(huán)即可
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 已經(jīng)存在該key的情況時(shí)诈闺,將對(duì)應(yīng)的節(jié)點(diǎn)的value設(shè)置為新的value
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 插入成功后渴庆,判斷實(shí)際存在的鍵值對(duì)數(shù)量 size 是否超多了最大容量 threshold,如果超過(guò)雅镊,進(jìn)行擴(kuò)容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

紅黑樹(shù)結(jié)構(gòu)的putVal方法:

final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                int h, K k, V v) {
    Class<?> kc = null;
    boolean searched = false;
    TreeNode<K,V> root = (parent != null) ? root() : this;
    for (TreeNode<K,V> p = root;;) {
        int dir, ph; K pk;
        if ((ph = p.hash) > h)
            dir = -1;
        else if (ph < h)
            dir = 1;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        else if ((kc == null &&
                    (kc = comparableClassFor(k)) == null) ||
                    (dir = compareComparables(kc, k, pk)) == 0) {
            if (!searched) {
                TreeNode<K,V> q, ch;
                searched = true;
                if (((ch = p.left) != null &&
                        (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                        (q = ch.find(h, k, kc)) != null))
                    return q;
            }
            dir = tieBreakOrder(k, pk);
        }

        TreeNode<K,V> xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node<K,V> xpn = xp.next;
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((TreeNode<K,V>)xpn).prev = x;
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }
}

get()方法

get(key) 方法時(shí)獲取 key 的 hash 值襟雷,計(jì)算 hash&(n-1) 得到在鏈表數(shù)組中的位置 first=tab[hash&(n-1)], 先判斷 first 的 key 是否與參數(shù) key 相等,不等就遍歷后面的鏈表找到相同的 key 值返回對(duì)應(yīng)的 Value 值即可仁烹。

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

// 根據(jù)哈希表元素個(gè)數(shù)與哈希值求模(使用的公式是 (n - 1) &hash)得到 key 所在的桶的頭結(jié)點(diǎn)耸弄,如果頭節(jié)點(diǎn)恰好是紅黑樹(shù)節(jié)點(diǎn),就調(diào)用紅黑樹(shù)節(jié)點(diǎn)的 getTreeNode() 方法卓缰,否則就遍歷鏈表節(jié)點(diǎ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;
        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;
}

resize()方法

擴(kuò)容 (resize) 就是重新計(jì)算容量计呈,向 HashMap 對(duì)象里不停的添加元素,而 HashMap 對(duì)象內(nèi)部的數(shù)組無(wú)法裝載更多的元素時(shí)僚饭,對(duì)象就需要擴(kuò)大數(shù)組的長(zhǎng)度震叮,以便能裝入更多的元素胧砰。當(dāng)然 Java 里的數(shù)組是無(wú)法自動(dòng)擴(kuò)容的鳍鸵,方法是使用一個(gè)新的數(shù)組代替已有的容量小的數(shù)組,就像我們用一個(gè)小桶裝水尉间,如果想裝更多的水偿乖,就得換大水桶。

由于需要考慮 hash 沖突解決時(shí)采用的可能是鏈表也可能是紅黑樹(shù)的方式哲嘲,因此 resize 方法相比 JDK7 中復(fù)雜了一些贪薪。

rehashing 觸發(fā)的條件:1、超過(guò)默認(rèn)容量 * 加載因子眠副;2画切、加載因子不靠譜,比如遠(yuǎn)大于 1囱怕。

在 HashMap 進(jìn)行擴(kuò)容時(shí)霍弹,會(huì)進(jìn)行 2 倍擴(kuò)容毫别,而且會(huì)將哈希碰撞處的數(shù)據(jù)再次分散開(kāi)來(lái),一部分依照新的 hash 索引值呆在 “原處”典格,一部分加上偏移量移動(dòng)到新的地方岛宦。

具體步驟為:

  1. 首先計(jì)算 resize() 后的新的 capacity 和 threshold 值。如果原有的 capacity 大于零則將 capacity 增加一倍耍缴,否則設(shè)置成默認(rèn)的 capacity砾肺。
  2. 創(chuàng)建新的數(shù)組,大小是新的 capacity
  3. 將舊數(shù)組的元素放置到新數(shù)組中
final Node<K,V>[] resize() {
    // 將字段引用copy到局部變量表防嗡,這樣在之后的使用時(shí)可以減少getField指令的調(diào)用
    Node<K,V>[] oldTab = table;
    // oldCap為原數(shù)組的大小或當(dāng)空時(shí)為0
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 如果超過(guò)最大容量1>>30变汪,無(wú)法再擴(kuò)充table,只能改變閾值
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 新的數(shù)組的大小是舊數(shù)組的兩倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 當(dāng)舊的的數(shù)組大小大于等于默認(rèn)大小時(shí)蚁趁,threshold也擴(kuò)大一倍
            newThr = oldThr << 1;
    }
    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"})
    // 創(chuàng)建容量為newCap的newTab疫衩,并將oldTab中的Node遷移過(guò)來(lái),這里需要考慮鏈表和tree兩種情況荣德。
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 將原數(shù)組中的數(shù)組復(fù)制到新數(shù)組中
    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)
                    // 如果e是該bucket唯一的一個(gè)元素闷煤,則直接賦值到新數(shù)組中
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // split方法會(huì)將樹(shù)分割為lower 和upper tree兩個(gè)樹(shù),如果子樹(shù)的節(jié)點(diǎn)數(shù)小于了UNTREEIFY_THRESHOLD閾值涮瞻,則將樹(shù)untreeify鲤拿,將節(jié)點(diǎn)都存放在newTab中。
                    // TreeNode的情況則使用TreeNode中的split方法將這個(gè)樹(shù)分成兩個(gè)小樹(shù)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order 保持順序
                    // 否則則創(chuàng)建兩個(gè)鏈表用來(lái)存放要放的數(shù)據(jù)署咽,hash值&oldCap為0的(即oldCap的1的位置的和hash值的同樣的位置都是1近顷,同樣是基于capacity是2的次方這一前提)為low鏈表,反之為high鏈表, 通過(guò)這種方式將舊的數(shù)據(jù)分到兩個(gè)鏈表中再放到各自對(duì)應(yīng)余數(shù)的位置
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 按照e.hash值區(qū)分放在loTail后還是hiTail后
                        if ((e.hash & oldCap) == 0) {
                            // 運(yùn)算結(jié)果為0的元素宁否,用lo記錄并連接成新的鏈表
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            // 運(yùn)算結(jié)果不為0的數(shù)據(jù)窒升,用li記錄
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 處理完之后放到新數(shù)組中
                    if (loTail != null) {
                        loTail.next = null;
                        // lo仍然放在“原處”,這個(gè)“原處”是根據(jù)新的hash值算出來(lái)的
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        // li放在j+oldCap位置
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

下面舉個(gè)例子說(shuō)明下擴(kuò)容過(guò)程慕匠。
假設(shè)了我們的 hash 算法就是簡(jiǎn)單的用 key mod 一下表的大斜バ搿(也就是數(shù)組的長(zhǎng)度)。其中的哈希桶數(shù)組 table 的 size=2台谊, 所以 key = 3蓉媳、7、5锅铅,put 順序依次為 5酪呻、7、3盐须。在 mod 2 以后都沖突在 table[1] 這里了玩荠。這里假設(shè)負(fù)載因子 loadFactor=1,即當(dāng)鍵值對(duì)的實(shí)際大小 size 大于 table 的實(shí)際大小時(shí)進(jìn)行擴(kuò)容。接下來(lái)的三個(gè)步驟是哈希桶數(shù)組 resize 成 4阶冈,然后所有的 Node 重新 rehash 的過(guò)程屉凯。

jdk1.7擴(kuò)容例圖.png

下面我們講解下 JDK1.8 做了哪些優(yōu)化。
經(jīng)過(guò)觀測(cè)可以發(fā)現(xiàn)眼溶,我們使用的是 2 次冪的擴(kuò)展 (指長(zhǎng)度擴(kuò)為原來(lái) 2 倍)悠砚,所以,元素的位置要么是在原位置堂飞,要么是在原位置再移動(dòng) 2 次冪的位置灌旧。看下圖可以明白這句話的意思绰筛,n 為 table 的長(zhǎng)度枢泰,圖(a)表示擴(kuò)容前的 key1 和 key2 兩種 key 確定索引位置的示例,圖(b)表示擴(kuò)容后 key1 和 key2 兩種 key 確定索引位置的示例铝噩,其中 hash1 是 key1 對(duì)應(yīng)的哈希與高位運(yùn)算結(jié)果衡蚂。

hashMap 1.8 哈希算法例圖1.png

元素在重新計(jì)算 hash 之后,因?yàn)?n 變?yōu)?2 倍骏庸,那么 n-1 的 mask 范圍在高位多 1bit(紅色)毛甲,因此新的 index 就會(huì)發(fā)生這樣的變化:

hashMap 1.8 哈希算法例圖2.png

因此,我們?cè)跀U(kuò)充 HashMap 的時(shí)候具被,不需要像 JDK1.7 的實(shí)現(xiàn)那樣重新計(jì)算 hash玻募,只需要看看原來(lái)的 hash 值新增的那個(gè) bit 是 1 還是 0 就好了,是 0 的話索引沒(méi)變一姿,是 1 的話索引變成 “原索引 + oldCap”七咧,可以看看下圖為 16 擴(kuò)充為 32 的 resize 示意圖:


jdk1.8 hashMap擴(kuò)容例圖.png

這個(gè)設(shè)計(jì)確實(shí)非常的巧妙,既省去了重新計(jì)算 hash 值的時(shí)間叮叹,而且同時(shí)艾栋,由于新增的 1bit 是 0 還是 1 可以認(rèn)為是隨機(jī)的,因此 resize 的過(guò)程蛉顽,均勻的把之前的沖突的節(jié)點(diǎn)分散到新的 bucket 了蝗砾。這一塊就是 JDK1.8 新增的優(yōu)化點(diǎn)。

紅黑樹(shù)結(jié)構(gòu)

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links 父節(jié)點(diǎn)
    TreeNode<K,V> left;    // 左子樹(shù)
    TreeNode<K,V> right;   // 右子樹(shù)
    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);
    }
}

樹(shù)形化操作

  1. 根據(jù)哈希表中元素個(gè)數(shù)確定是擴(kuò)容還是樹(shù)形化
  2. 如果是樹(shù)形化遍歷桶中的元素蜂林,創(chuàng)建相同個(gè)數(shù)的樹(shù)形節(jié)點(diǎn)遥诉,復(fù)制內(nèi)容,建立起聯(lián)系
  3. 然后讓桶第一個(gè)元素指向新建的樹(shù)頭結(jié)點(diǎn)噪叙,替換桶的鏈表內(nèi)容為樹(shù)形內(nèi)容
// MIN_TREEIFY_CAPACITY 的值為64,若當(dāng)前table的length不夠霉翔,則resize()
// 將桶內(nèi)所有的 鏈表節(jié)點(diǎn) 替換成 紅黑樹(shù)節(jié)點(diǎn)
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 如果當(dāng)前哈希表為空睁蕾,或者哈希表中元素的個(gè)數(shù)小于樹(shù)形化閾值(默認(rèn)為 64),就去新建(擴(kuò)容)
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // 如果哈希表中的元素個(gè)數(shù)超過(guò)了樹(shù)形化閾值,則進(jìn)行樹(shù)形化
    // e 是哈希表中指定位置桶里的鏈表節(jié)點(diǎn)子眶,從第一個(gè)開(kāi)始
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 紅黑樹(shù)的頭瀑凝、尾節(jié)點(diǎn)
        TreeNode<K,V> hd = null, tl = null;
        do {
            // 新建一個(gè)樹(shù)形節(jié)點(diǎn),內(nèi)容和當(dāng)前鏈表節(jié)點(diǎn) e 一致
            TreeNode<K,V> p = replacementTreeNode(e, null);
            // 確定樹(shù)頭節(jié)點(diǎn)
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        // 讓桶的第一個(gè)元素指向新建的紅黑樹(shù)頭結(jié)點(diǎn)臭杰,以后這個(gè)桶里的元素就是紅黑樹(shù)而不是鏈表了
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}

size()方法

HashMap 的大小很簡(jiǎn)單粤咪,不是實(shí)時(shí)計(jì)算的,而是每次新增加 Entry 的時(shí)候渴杆,size 就遞增寥枝。刪除的時(shí)候就遞減〈沤保空間換時(shí)間的做法囊拜。因?yàn)樗皇蔷€程安全的。完全可以這么做比搭,效率高冠跷。

面試問(wèn)題

HashMap 的實(shí)現(xiàn)原理

HashMap 是典型的空間換時(shí)間的一種技術(shù)手段。

  • 如何解決 hash 沖突
  • loadFactor 等核心概念
  • 擴(kuò)容機(jī)制

構(gòu)造函數(shù)中 initialCapacity 與 loadFactor 兩個(gè)參數(shù)

HashMap(int initialCapacity, float loadFactor):構(gòu)造一個(gè)指定容量和加載因子的空 HashMap

在這里提到了兩個(gè)參數(shù):初始容量身诺,加載因子蜜托。這兩個(gè)參數(shù)是影響 HashMap 性能的重要參數(shù),其中容量表示哈希表中桶的數(shù)量霉赡,初始容量是創(chuàng)建哈希表時(shí)的容量盗冷,加載因子是哈希表在其容量自動(dòng)增加之前可以達(dá)到多滿(mǎn)的一種尺度,它衡量的是一個(gè)散列表的空間的使用程度同廉,負(fù)載因子越大表示散列表的裝填程度越高仪糖,反之愈小。對(duì)于使用鏈表法的散列表來(lái)說(shuō)迫肖,查找一個(gè)元素的平均時(shí)間是 O(1+a)锅劝,因此如果負(fù)載因子越大,對(duì)空間的利用更充分蟆湖,然而后果是查找效率的降低故爵;如果負(fù)載因子太小,那么散列表的數(shù)據(jù)將過(guò)于稀疏隅津,對(duì)空間造成嚴(yán)重浪費(fèi)诬垂。系統(tǒng)默認(rèn)負(fù)載因子為 0.75,一般情況下我們是無(wú)需修改的伦仍。

initialCapacity 和 loadFactor 參數(shù)設(shè)什么樣的值好呢结窘?

initialCapacity 的默認(rèn)值是 16,有些人可能會(huì)想如果內(nèi)存足夠充蓝,是不是可以將 initialCapacity 設(shè)大一些隧枫,即使用不了這么大喉磁,就可避免擴(kuò)容導(dǎo)致的效率的下降,反正無(wú)論 initialCapacity 大小官脓,我們使用的 get 和 put 方法都是常數(shù)復(fù)雜度的协怒。這么說(shuō)沒(méi)什么不對(duì),但是可能會(huì)忽略一點(diǎn)卑笨,實(shí)際的程序可能不僅僅使用 get 和 put 方法孕暇,也有可能使用迭代器,如 initialCapacity 容量較大赤兴,那么會(huì)使迭代器效率降低妖滔。所以理想的情況還是在使用 HashMap 前估計(jì)一下數(shù)據(jù)量。

加載因子默認(rèn)值是 0.75搀缠,是 JDK 權(quán)衡時(shí)間和空間效率之后得到的一個(gè)相對(duì)優(yōu)良的數(shù)值铛楣。如果這個(gè)值過(guò)大,雖然空間利用率是高了艺普,但是對(duì)于 HashMap 中的一些方法的效率就下降了簸州,包括 get 和 put 方法,會(huì)導(dǎo)致每個(gè) hash 桶所附加的鏈表增長(zhǎng)歧譬,影響存取效率岸浑。如果比較小,除了導(dǎo)致空間利用率較低外沒(méi)有什么壞處瑰步,只要有的是內(nèi)存矢洲,畢竟現(xiàn)在大多數(shù)人把時(shí)間看的比空間重要。但是實(shí)際中還是很少有人會(huì)將這個(gè)值設(shè)置的低于 0.5缩焦。

size 為什么必須是 2 的整數(shù)次冪

Java 編程:淺析 HashMap 中數(shù)組的 size 為什么必須是 2 的整數(shù)次冪

這是為了服務(wù)key映射到index的Hash算法的读虏,公式index=hashcode(key)&(length-1),初始長(zhǎng)度(16-1)袁滥,二進(jìn)制為1111&hashcode結(jié)果為hashcode最后四位盖桥,能最大程度保持平均,二的冪數(shù)保證二進(jìn)制為1题翻,保持hashcode最后四位揩徊。這種算法在保持分布均勻之外,效率也非常高嵌赠。

HashMap 的 key 為什么一般用字符串比較多塑荒,能用其他對(duì)象,或者自定義的對(duì)象嗎姜挺?為什么齿税?

能用其他對(duì)象,必須是immutable的初家,但是自實(shí)現(xiàn)的類(lèi)必須Override兩個(gè)方法:equals()和hashCode()偎窘。否則會(huì)調(diào)用默認(rèn)的Object類(lèi)的對(duì)應(yīng)方法乌助。

為什么HashMap要允許key和value都能為null呢溜在?

解釋一:首先陌知,HashTable 是一個(gè)過(guò)時(shí)的類(lèi),不應(yīng)該再使用了掖肋。
當(dāng)時(shí)不允許是因?yàn)橄M總€(gè) key 都會(huì)實(shí)現(xiàn) hashCode 和 equals 方法仆葡。而 null 顯然沒(méi)有。后來(lái)志笼,大家都意識(shí)到 null 值的重要性沿盅,所以 HashMap 允許 null 值的 key 和 value。當(dāng) key 為 null 時(shí)纫溃,HashMap 將會(huì)把 key-value pair 存放在一個(gè)特殊的位置腰涧,比如第一個(gè)槽位里。

解釋二:ConcurrentHashmap 和 Hashtable 都是支持并發(fā)的紊浩,這樣會(huì)有一個(gè)問(wèn)題窖铡,當(dāng)你通過(guò) get(k) 獲取對(duì)應(yīng)的 value 時(shí),如果獲取到的是 null 時(shí)坊谁,你無(wú)法判斷费彼,它是 put() 的時(shí)候 value 為 null,還是這個(gè) key 從來(lái)沒(méi)有做過(guò)映射口芍。HashMap 是非并發(fā)的箍铲,可以通過(guò) contains(key) 來(lái)做這個(gè)判斷。而支持并發(fā)的 Map 在調(diào)用 m.contains(key)和 m.get(key),m 可能已經(jīng)不同了鬓椭。

為什么需要使用加載因子颠猴,為什么需要擴(kuò)容呢?

因?yàn)槿绻畛浔群艽笮∪荆f(shuō)明利用的空間很多翘瓮,如果一直不進(jìn)行擴(kuò)容的話,鏈表就會(huì)越來(lái)越長(zhǎng)氧映,這樣查找的效率很低春畔,因?yàn)殒湵淼拈L(zhǎng)度很大(當(dāng)然最新版本使用了紅黑樹(shù)后會(huì)改進(jìn)很多),擴(kuò)容之后岛都,將原來(lái)鏈表數(shù)組的每一個(gè)鏈表分成奇偶兩個(gè)子鏈表分別掛在新鏈表數(shù)組的散列位置律姨,這樣就減少了每個(gè)鏈表的長(zhǎng)度,增加查找效率臼疫。

HashMap 本來(lái)是以空間換時(shí)間择份,所以填充比沒(méi)必要太大。但是填充比太小又會(huì)導(dǎo)致空間浪費(fèi)烫堤。如果關(guān)注內(nèi)存荣赶,填充比可以稍大凤价,如果主要關(guān)注查找性能,填充比可以稍小拔创。

使用紅黑樹(shù)的改進(jìn)

在 java jdk8 中對(duì) HashMap 的源碼進(jìn)行了優(yōu)化利诺,在 jdk7 中,HashMap 處理 “碰撞” 的時(shí)候剩燥,都是采用鏈表來(lái)存儲(chǔ)慢逾,當(dāng)碰撞的結(jié)點(diǎn)很多時(shí),查詢(xún)時(shí)間是 O(n)灭红。
在 jdk8 中侣滩,HashMap 處理 “碰撞” 增加了紅黑樹(shù)這種數(shù)據(jù)結(jié)構(gòu),當(dāng)碰撞結(jié)點(diǎn)較少時(shí)变擒,采用鏈表存儲(chǔ)君珠,當(dāng)較大時(shí)(>8 個(gè)),采用紅黑樹(shù)(特點(diǎn)是查詢(xún)時(shí)間是 O(logn))存儲(chǔ)(有一個(gè)閥值控制娇斑,大于閥值 (8 個(gè))策添,將鏈表存儲(chǔ)轉(zhuǎn)換成紅黑樹(shù)存儲(chǔ))

問(wèn)題分析:
哈希碰撞會(huì)對(duì) hashMap 的性能帶來(lái)災(zāi)難性的影響。如果多個(gè) hashCode() 的值落到同一個(gè)桶內(nèi)的時(shí)候悠菜,這些值是存儲(chǔ)到一個(gè)鏈表中的舰攒。最壞的情況下,所有的 key 都映射到同一個(gè)桶中悔醋,這樣 hashmap 就退化成了一個(gè)鏈表——查找時(shí)間從 O(1) 到 O(n)摩窃。

隨著 HashMap 的大小的增長(zhǎng),get() 方法的開(kāi)銷(xiāo)也越來(lái)越大芬骄。由于所有的記錄都在同一個(gè)桶里的超長(zhǎng)鏈表內(nèi)猾愿,平均查詢(xún)一條記錄就需要遍歷一半的列表。
JDK1.8HashMap 的紅黑樹(shù)是這樣解決的:
如果某個(gè)桶中的記錄過(guò)大的話(當(dāng)前是 TREEIFY_THRESHOLD = 8)账阻,HashMap 會(huì)動(dòng)態(tài)的使用一個(gè)專(zhuān)門(mén)的 treemap 實(shí)現(xiàn)來(lái)替換掉它蒂秘。這樣做的結(jié)果會(huì)更好,是 O(logn)淘太,而不是糟糕的 O(n)姻僧。

它是如何工作的?前面產(chǎn)生沖突的那些 KEY 對(duì)應(yīng)的記錄只是簡(jiǎn)單的追加到一個(gè)鏈表后面蒲牧,這些記錄只能通過(guò)遍歷來(lái)進(jìn)行查找撇贺。但是超過(guò)這個(gè)閾值后 HashMap 開(kāi)始將列表升級(jí)成一個(gè)二叉樹(shù),使用哈希值作為樹(shù)的分支變量冰抢,如果兩個(gè)哈希值不等松嘶,但指向同一個(gè)桶的話,較大的那個(gè)會(huì)插入到右子樹(shù)里挎扰。如果哈希值相等翠订,HashMap 希望 key 值最好是實(shí)現(xiàn)了 Comparable 接口的巢音,這樣它可以按照順序來(lái)進(jìn)行插入。這對(duì) HashMap 的 key 來(lái)說(shuō)并不是必須的尽超,不過(guò)如果實(shí)現(xiàn)了當(dāng)然最好官撼。如果沒(méi)有實(shí)現(xiàn)這個(gè)接口,在出現(xiàn)嚴(yán)重的哈希碰撞的時(shí)候橙弱,你就并別指望能獲得性能提升了歧寺。

HashMap 的 key 和 value 都能為 null 么燥狰?如果 key 能為 null棘脐,那么它是怎么樣查找值的?

如果 key 為 null龙致,則直接從哈希表的第一個(gè)位置 table[0] 對(duì)應(yīng)的鏈表上查找,由 putForNullKey()實(shí)現(xiàn)蛀缝。記住,key 為 null 的鍵值對(duì)永遠(yuǎn)都放在以 table[0] 為頭結(jié)點(diǎn)的鏈表中目代。

平時(shí)在使用 HashMap 時(shí)一般使用什么類(lèi)型的元素作為 Key屈梁?

面試者通常會(huì)回答,使用 String 或者 Integer 這樣的類(lèi)榛了。這個(gè)時(shí)候可以繼續(xù)追問(wèn)為什么使用 String在讶、Integer 呢?這些類(lèi)有什么特點(diǎn)霜大?如果面試者有很好的思考构哺,可以回答出這些類(lèi)是 Immutable 的,并且這些類(lèi)已經(jīng)很規(guī)范的覆寫(xiě)了 hashCode() 以及 equals() 方法战坤。作為不可變類(lèi)天生是線程安全的曙强,而且可以很好的優(yōu)化比如可以緩存 hash 值,避免重復(fù)計(jì)算等等途茫,那么基本上這道題算是過(guò)關(guān)了碟嘴。

在 HashMap 中使用可變對(duì)象作為 Key 帶來(lái)的問(wèn)題:如果 HashMap Key 的哈希值在存儲(chǔ)鍵值對(duì)后發(fā)生改變,Map 可能再也查找不到這個(gè) Entry 了囊卜。詳見(jiàn):一道面試題看 HashMap 的存儲(chǔ)方式

Key對(duì)應(yīng)的hashCode()方法

對(duì)于非 String 類(lèi)型的 key娜扇,hash() 使用 key 的 hashCode() 計(jì)算出該 key-value pair 對(duì)應(yīng)在數(shù)組的索引 (String 類(lèi)型的 key 有另一套計(jì)算 hash() 的方法,再次不做贅述)栅组,在 get() 方法中會(huì)使用 key 的 equal() 方法判斷數(shù)組中的中的 key 是否與傳入的 key 相等雀瓢。由此可見(jiàn),在 HashMap 的使用中笑窜,key 類(lèi)型中定義的 hashCode() 和 equal() 方法都是非常重要的致燥。假設(shè) key 內(nèi)部的值發(fā)生變化,導(dǎo)致 hashCode()/equal() 的結(jié)果改變排截,那么該 key 在 HashMap 中的存取則會(huì)產(chǎn)生問(wèn)題嫌蚤。

如何創(chuàng)建不可變類(lèi)(Immutable)/如果讓你實(shí)現(xiàn)一個(gè)自定義的 class 作為 HashMap 的 key 該如何實(shí)現(xiàn)辐益?

如何創(chuàng)建不可變(Immutable)的 Java 類(lèi)或?qū)ο?/a>
HashMap 的 key 可以是可變的對(duì)象嗎
危險(xiǎn)!在 HashMap 中將可變對(duì)象用作 Key
如何使用建造者模式 (Builder Pattern) 創(chuàng)建不可變類(lèi)

Immutable Objects 就是那些一旦被創(chuàng)建脱吱,它們的狀態(tài)就不能被改變的 Objects智政,每次對(duì)他們的改變都是產(chǎn)生了新的 immutable 的對(duì)象,而 mutable Objects 就是那些創(chuàng)建后箱蝠,狀態(tài)可以被改變的 Objects续捂。
舉個(gè)例子:String 是 immutable 的,每次對(duì)于 String 對(duì)象的修改都將產(chǎn)生一個(gè)新的 String 對(duì)象宦搬,而原來(lái)的對(duì)象保持不變牙瓢。而 StringBuilder 是 mutable,因?yàn)槊看螌?duì)于它的對(duì)象的修改都作用于該對(duì)象本身间校,并沒(méi)有產(chǎn)生新的對(duì)象矾克。
但有的時(shí)候 String 的 immutable 特性也會(huì)引起安全問(wèn)題,這就是密碼應(yīng)該存放在字符數(shù)組中而不是 String 中的原因憔足!

要寫(xiě)出這樣的類(lèi)胁附,需要遵循以下幾個(gè)原則:

  1. immutable 對(duì)象的狀態(tài)在創(chuàng)建之后就不能發(fā)生改變,任何對(duì)它的改變都應(yīng)該產(chǎn)生一個(gè)新的對(duì)象滓彰。
  2. Immutable 類(lèi)的所有的屬性都應(yīng)該是 final 的控妻。
  3. 對(duì)象必須被正確的創(chuàng)建,比如:對(duì)象引用在對(duì)象創(chuàng)建過(guò)程中不能泄露 (leak)揭绑。
  4. 對(duì)象應(yīng)該是 final 的弓候,以此來(lái)限制子類(lèi)繼承父類(lèi),以避免子類(lèi)改變了父類(lèi)的 immutable 特性洗做。
  5. 如果類(lèi)中包含 mutable 類(lèi)對(duì)象弓叛,那么返回給客戶(hù)端的時(shí)候,返回該對(duì)象的一個(gè)拷貝诚纸,而不是該對(duì)象本身(該條可以歸為第一條中的一個(gè)特例)
public class MutableKey {
    private int i;
    private int j;

    public MutableKey(int i, int j) {
        this.i = i;
        this.j = j;
    }

    public final int getI() {
        return i;
    }

    public final void setI(int i) {
        this.i = i;
    }

    public final int getJ() {
        return j;
    }

    public final void setJ(int j) {
        this.j = j;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + i;
        result = prime * result + j;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof MutableKey)) {
            return false;
        }
        MutableKey other = (MutableKey) obj;
        if (i != other.i) {
            return false;
        }
        if (j != other.j) {
            return false;
        }
        return true;
    }
    
    public static void main(String[] args) {
        // Object created
        MutableKey key = new MutableKey(10, 20);
        System.out.println("Hash code: " + key.hashCode());

        // Object State is changed after object creation.
        key.setI(30);
        key.setJ(40);
        System.out.println("Hash code: " + key.hashCode());
    }
}
public class MutableSafeKey {
    // Cannot be changed once object is created. No setter for this field.
    private final int id;
    private String name;

    public MutableSafeKey(final int id) {
        this.id = id;
    }

    public final String getName() {
        return name;
    }

    public final void setName(final String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    // Hash code depends only on 'id' which cannot be
    // changed once object is created. So hash code will not change
    // on object's state change
    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + id;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        MutableSafeKey other = (MutableSafeKey) obj;
        if (id != other.id)
            return false;
        return true;
    }
}

你能設(shè)計(jì)一個(gè)算法(輸入是 java 源文件)撰筷,判斷一個(gè)類(lèi)是否是 Immutable 的嗎?

如何衡量一個(gè) hash 算法的好壞

hashCode 不要求唯一但是要盡可能的均勻分布畦徘,而且算法效率要盡可能的快毕籽。

HashMap 中 hash 函數(shù)怎么是是實(shí)現(xiàn)的?

高 16bit 不變井辆,低 16bit 和高 16bit 做了一個(gè)異或:(n - 1) & hash --> 得到下標(biāo)

拓展:為什么 h = 31 * h + val[off++]; 這一行使用31 关筒,而不是別的數(shù)字,這是一個(gè)魔術(shù)嗎杯缺?

HashMap 怎樣解決哈希沖突蒸播,講一下擴(kuò)容過(guò)程。

JDK 使用了鏈地址法,hash 表的每個(gè)元素又分別鏈接著一個(gè)單鏈表袍榆,元素為頭結(jié)點(diǎn)胀屿,如果不同的 key 映射到了相同的下標(biāo),那么就使用頭插法包雀,插入到該元素對(duì)應(yīng)的鏈表宿崭。

擴(kuò)容過(guò)程:

  • 將新節(jié)點(diǎn)加到鏈表后
  • 容量擴(kuò)充為原來(lái)的兩倍,然后對(duì)每個(gè)節(jié)點(diǎn)重新計(jì)算哈希值才写。
  • 這個(gè)值只可能在兩個(gè)地方葡兑,一個(gè)是原下標(biāo)的位置,另一種是在下標(biāo)為 <原下標(biāo) + 原容量> 的位置赞草。

哈希沖突的常見(jiàn)解決方法:

  • 開(kāi)放定址法(線性探測(cè)再散列讹堤,二次探測(cè)再散列,偽隨機(jī)探測(cè)再散列)
  • 再哈希法房资,就是在原 hash 函數(shù)的基礎(chǔ)蜕劝,再次執(zhí)行 hash 算法
  • 鏈地址法,各種處理哈希碰撞的方法中轰异,這種最簡(jiǎn)單,也是 HashMap 中使用的方法
  • 建立一個(gè)公共溢出區(qū)

failure case/resize()

Load factor(default to 75%) 和 Initial capacity(default to 16) 是 HashMap 表的兩個(gè)重要屬性暑始,如果 HashMap 中 entry 數(shù)量超過(guò)了 threshold(loadfactor *capacity) 搭独,那么 HashMap 就不得不擴(kuò)充 capacity(否則 hash collison 發(fā)生的概率就會(huì)大大增加,導(dǎo)致整個(gè) HashMap 性能下降)廊镜,擴(kuò)充 capacity 是一件比較麻煩的事情牙肝,因?yàn)閿?shù)組的連續(xù)性,HashMap 不得不開(kāi)辟一塊更大數(shù)組嗤朴,還要把原來(lái)的 entries 全部 transfer 到新的數(shù)組中配椭,在某些情況下還需要重新計(jì)算 key 的 hash() 結(jié)果。另一方面雹姊,HashMap 的 capacity 也不是越大越好股缸,事實(shí)上 HashMap 的遍歷本質(zhì)上是基于內(nèi)部數(shù)組的遍歷,如果內(nèi)部數(shù)組是無(wú)意義的大吱雏,那么遍歷 HashMap 相對(duì)來(lái)說(shuō)不是特別高效敦姻。

為什么 HashMap 是線程不安全的,實(shí)際會(huì)如何體現(xiàn)歧杏?

第一镰惦,如果多個(gè)線程同時(shí)使用 put 方法添加元素
假設(shè)正好存在兩個(gè) put 的 key 發(fā)生了碰撞 (hash 值一樣),那么根據(jù) HashMap 的實(shí)現(xiàn)犬绒,這兩個(gè) key 會(huì)添加到數(shù)組的同一個(gè)位置旺入,這樣最終就會(huì)發(fā)生其中一個(gè)線程的 put 的數(shù)據(jù)被覆蓋。

第二凯力,如果多個(gè)線程同時(shí)檢測(cè)到元素個(gè)數(shù)超過(guò)數(shù)組大小 * loadFactor
這樣會(huì)發(fā)生多個(gè)線程同時(shí)對(duì) hash 數(shù)組進(jìn)行擴(kuò)容茵瘾,都在重新計(jì)算元素位置以及復(fù)制數(shù)據(jù)急膀,但是最終只有一個(gè)線程擴(kuò)容后的數(shù)組會(huì)賦給 table,也就是說(shuō)其他線程的都會(huì)丟失龄捡,并且各自線程 put 的數(shù)據(jù)也丟失卓嫂。且會(huì)引起死循環(huán)的錯(cuò)誤。

具體細(xì)節(jié)上的原因聘殖,可以參考:不正當(dāng)使用 HashMap 導(dǎo)致 cpu 100% 的問(wèn)題追究

HashMap 不是線程安全的晨雳,你怎么理解線程安全。原理是什么奸腺?幾種方式避免線程安全的問(wèn)題

如何線程安全的使用 HashMap

  1. 直接使用 Hashtable餐禁,但是當(dāng)一個(gè)線程訪問(wèn) HashTable 的同步方法時(shí),其他線程如果也要訪問(wèn)同步方法突照,會(huì)被阻塞住帮非。舉個(gè)例子,當(dāng)一個(gè)線程使用 put 方法時(shí)讹蘑,另一個(gè)線程不但不可以使用 put 方法末盔,連 get 方法都不可以,效率很低座慰,現(xiàn)在基本不會(huì)選擇它了陨舱。
  2. HashMap 可以通過(guò)下面的語(yǔ)句進(jìn)行同步:
    Collections.synchronizeMap(hashMap);
  3. 直接使用 JDK 5 之后的 ConcurrentHashMap显押。

HashTable 和 HashMap 的區(qū)別有哪些掖举?

HashMap 和 Hashtable 都實(shí)現(xiàn)了 Map 接口,但決定用哪一個(gè)之前先要弄清楚它們之間的分別痹兜。主要的區(qū)別有:線程安全性蛮粮,同步 (synchronization)益缎,以及速度。

理解 HashMap 是 Hashtable 的輕量級(jí)實(shí)現(xiàn)(非線程安全的實(shí)現(xiàn)然想,hashtable 是非輕量級(jí)莺奔,線程安全的),都實(shí)現(xiàn) Map 接口又沾,主要區(qū)別在于:

  1. 由于 HashMap 非線程安全弊仪,在只有一個(gè)線程訪問(wèn)的情況下,效率要高于 HashTable
  2. HashMap 允許將 null 作為一個(gè) entry 的 key 或者 value杖刷,而 Hashtable 不允許励饵。
  3. HashMap 把 Hashtable 的 contains 方法去掉了,改成 containsValue 和 containsKey滑燃。因?yàn)?contains 方法容易讓人引起誤解役听。
  4. Hashtable 繼承自陳舊的 Dictionary 類(lèi),而 HashMap 是 Java1.2 引進(jìn)的 Map 的一個(gè)實(shí)現(xiàn)。
  5. Hashtable 和 HashMap 擴(kuò)容的方法不一樣典予,HashTable 中 hash 數(shù)組默認(rèn)大小 11甜滨,擴(kuò)容方式是 old*2+1。HashMap 中 hash 數(shù)組的默認(rèn)大小是 16瘤袖,而且一定是 2 的指數(shù)衣摩,增加為原來(lái)的 2 倍,沒(méi)有加 1捂敌。
  6. 兩者通過(guò) hash 值散列到 hash 表的算法不一樣艾扮,HashTbale 是古老的除留余數(shù)法,直接使用 hashcode占婉,而后者是強(qiáng)制容量為 2 的冪泡嘴,重新根據(jù) hashcode 計(jì)算 hash 值,在使用 hash 位與 (hash 表長(zhǎng)度 – 1)逆济,也等價(jià)取膜酌予,但更加高效,取得的位置更加分散奖慌,偶數(shù)抛虫,奇數(shù)保證了都會(huì)分散到。前者就不能保證升薯。
  7. 另一個(gè)區(qū)別是 HashMap 的迭代器 (Iterator) 是 fail-fast 迭代器莱褒,而 Hashtable 的 enumerator 迭代器不是 fail-fast 的。所以當(dāng)有其它線程改變了 HashMap 的結(jié)構(gòu)(增加或者移除元素)涎劈,將會(huì)拋出 ConcurrentModificationException,但迭代器本身的 remove() 方法移除元素則不會(huì)拋出 ConcurrentModificationException 異常阅茶。但這并不是一個(gè)一定發(fā)生的行為蛛枚,要看 JVM。這條同樣也是 Enumeration 和 Iterator 的區(qū)別脸哀。
    fail-fast 和 iterator 迭代器相關(guān)蹦浦。如果某個(gè)集合對(duì)象創(chuàng)建了 Iterator 或者 ListIterator,然后其它的線程試圖 “結(jié)構(gòu)上” 更改集合對(duì)象撞蜂,將會(huì)拋出 ConcurrentModificationException 異常盲镶。但其它線程可以通過(guò) set() 方法更改集合對(duì)象是允許的,因?yàn)檫@并沒(méi)有從 “結(jié)構(gòu)上” 更改集合蝌诡。但是假如已經(jīng)從結(jié)構(gòu)上進(jìn)行了更改溉贿,再調(diào)用 set() 方法,將會(huì)拋出 IllegalArgumentException 異常浦旱。
    結(jié)構(gòu)上的更改指的是刪除或者插入一個(gè)元素宇色,這樣會(huì)影響到 map 的結(jié)構(gòu)。
    該條說(shuō)白了就是在使用迭代器的過(guò)程中有其他線程在結(jié)構(gòu)上修改了 map,那么將拋出 ConcurrentModificationException宣蠕,這就是所謂 fail-fast 策略例隆。

引申擴(kuò)展:建議用 ConcurrentHashMap 代替 Hashtable。

為什么 HashTable 的默認(rèn)大小和 HashMap 不一樣抢蚀?

前面分析了镀层,Hashtable 的擴(kuò)容方法是乘 2 再 + 1,不是簡(jiǎn)單的乘 2皿曲,故 hashtable 保證了容量永遠(yuǎn)是奇數(shù)唱逢,結(jié)合之前分析 hashmap 的重算 hash 值的邏輯,就明白了谷饿,因?yàn)樵跀?shù)據(jù)分布在等差數(shù)據(jù)集合 (如偶數(shù)) 上時(shí)惶我,如果公差與桶容量有公約數(shù) n,則至少有 (n-1)/n 數(shù)量的桶是利用不到的博投,故之前的 hashmap 會(huì)在取模(使用位與運(yùn)算代替)哈希前先做一次哈希運(yùn)算绸贡,調(diào)整 hash 值。這里 hashtable 比較古老毅哗,直接使用了除留余數(shù)法听怕,那么就需要設(shè)置容量起碼不是偶數(shù)(除(近似)質(zhì)數(shù)求余的分散效果好)。而 JDK 開(kāi)發(fā)者選了 11虑绵。

參考資料

Java 8 系列之重新認(rèn)識(shí) HashMap - 美團(tuán)點(diǎn)評(píng)技術(shù)團(tuán)隊(duì)
通過(guò) HashMap尿瞭、HashSet 的源代碼分析其 Hash 存儲(chǔ)機(jī)制
HashMap 源碼分析
通過(guò)分析 JDK 源代碼研究 Hash 存儲(chǔ)機(jī)制
HashMap 深度分析
HashMap 源碼分析
HashMap 源碼分析
對(duì)比 HashMap 在 Java8 和 Java7 的源碼實(shí)現(xiàn)
深入理解 Java 之 HashMap 源碼解析
Java HashMap 源碼解析
JAVA 源碼分析 - HashMap 源碼分析 (一)
Java HashMap 工作原理及實(shí)現(xiàn)
遲到一年 HashMap 解讀
HashMap 實(shí)現(xiàn)原理分析
(轉(zhuǎn))Java HashMap 的死循環(huán)
【算法導(dǎo)論】紅黑樹(shù)詳解之一 (插入)
Hashes 10 Rehashing

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市翅睛,隨后出現(xiàn)的幾起案子声搁,更是在濱河造成了極大的恐慌,老刑警劉巖捕发,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疏旨,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡扎酷,警方通過(guò)查閱死者的電腦和手機(jī)檐涝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)法挨,“玉大人谁榜,你說(shuō)我怎么就攤上這事》材桑” “怎么了窃植?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)惫企。 經(jīng)常有香客問(wèn)我撕瞧,道長(zhǎng)陵叽,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任丛版,我火速辦了婚禮巩掺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘页畦。我一直安慰自己胖替,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布豫缨。 她就那樣靜靜地躺著独令,像睡著了一般。 火紅的嫁衣襯著肌膚如雪好芭。 梳的紋絲不亂的頭發(fā)上燃箭,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音舍败,去河邊找鬼招狸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛邻薯,可吹牛的內(nèi)容都是我干的裙戏。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼厕诡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼累榜!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起灵嫌,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤壹罚,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后寿羞,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體渔嚷,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年稠曼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片客年。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡霞幅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出量瓜,到底是詐尸還是另有隱情司恳,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布绍傲,位于F島的核電站扔傅,受9級(jí)特大地震影響耍共,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜猎塞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一试读、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧荠耽,春花似錦钩骇、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至慢叨,卻和暖如春纽匙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拍谐。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工烛缔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人赠尾。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓力穗,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親气嫁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子当窗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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