HashMap 靈魂拷問:從源碼找尋答案

前言

經(jīng)常用 HashMap,本來以為沒多少內(nèi)容,但是仔細(xì)研究之后發(fā)現(xiàn)還是有點(diǎn)東西的。本文通過一些問題結(jié)合源碼對 HashMap 進(jìn)行記錄,以便再次學(xué)習(xí)。

本文源碼基于 JDK1.8

問題來了

問題1: HashMap 數(shù)據(jù)結(jié)構(gòu)?

JDK 1.7 使用數(shù)組+鏈表,JDK1.8 使用數(shù)組+鏈表或紅黑樹結(jié)構(gòu)。

HashMap 數(shù)據(jù)結(jié)構(gòu)

問題2: HashMap 數(shù)據(jù)存放原理?

基本過程:

  1. 根據(jù)新增數(shù)據(jù) HashCode 和 數(shù)組長度 確認(rèn)下標(biāo),如果當(dāng)前下標(biāo)沒有數(shù)據(jù)則直接存放。
  2. 如果當(dāng)前下標(biāo)有數(shù)據(jù),則往該數(shù)據(jù)以鏈表結(jié)構(gòu)往后添加。
  3. 當(dāng)某個元素鏈表長度大于 8 之后,轉(zhuǎn)換為紅黑樹。下次再往樹結(jié)構(gòu)下標(biāo)添加數(shù)據(jù)時,添加到紅黑樹元素中。

源碼分析:

HashMap # putVal()

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 1.為空則初始化數(shù)組長度
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
     // 2. 如果當(dāng)前下標(biāo)元素鏈表為空,創(chuàng)建新結(jié)點(diǎn)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // 當(dāng)前下標(biāo)元素鏈表存在值,則往鏈表后添加
        Node<K,V> e; K k;
        // 3. 如果 key 存在,覆蓋 value
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4. 要添加的結(jié)點(diǎn)為樹結(jié)點(diǎn),則添加到樹結(jié)構(gòu)中
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 5.不是樹則遍歷鏈表
            for (int binCount = 0; ; ++binCount) {
                // 5.1 如果下一個結(jié)點(diǎn)為 null,則把新的結(jié)點(diǎn)添加即可
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 5.2 如果長度大于 8 放到數(shù)結(jié)構(gòu)中
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 5.3 如果key相同,賦值給 e 跳出循環(huán),后面進(jìn)行值覆蓋
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 6. 覆蓋值操作
        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)
        // 7.擴(kuò)容
        resize();
    afterNodeInsertion(evict);
    return null;
}
  1. 當(dāng) HashMap 為空時,調(diào)用 resize() 方法初始化 HashMap 的容量。默認(rèn)的初始容量為 DEFAULT_INITIAL_CAPACITY = 1 << 4 // 16 。
  2. 確定元素位置,創(chuàng)建新元素。

2.1 首先確定新元素的下標(biāo) i = (n - 1) & hash。n 是數(shù)組長度,那么 n-1 就是下標(biāo)最大值。與 hash 碼進(jìn)行與運(yùn)算,得出的結(jié)果必定是不大于最大下標(biāo)的。

比如數(shù)組長度為 4,最大下標(biāo)為 3 二進(jìn)制為 00..011。hash 值轉(zhuǎn)換為二進(jìn)制為 01...10。
進(jìn)行 & 運(yùn)算由于下標(biāo)前面都是補(bǔ)位的 0 ,& 運(yùn)算之后都為 0。所以只有最后兩位有意義。 11&10 = 10,下標(biāo)為 2。
如果這時 hash 值的最后兩位為 01,11&01=01 下標(biāo)為 1。

2.2 確定好下標(biāo)之后,調(diào)用 newNode 方法創(chuàng)建新結(jié)點(diǎn)并放置在下標(biāo)位置。

  1. p 不為 null 說明當(dāng)前下標(biāo)存在元素。
    如果新增元素的 hash 以及 key 都與當(dāng)前 p 元素一致,將 p 賦值給 e。
    到后面 6 中進(jìn)行判斷,將 e 的值覆蓋為新的值,也就是說原來 p 的 hash key 的位置值被覆蓋掉了。

  2. 如果結(jié)點(diǎn)是樹類型的,把傳入的信息生成一個新的樹結(jié)點(diǎn)并存放到樹結(jié)構(gòu)中。

  3. 不是樹類型,則遍歷該元素鏈表進(jìn)行處理。

  4. e 的值不為空,說明要傳入的 key 已經(jīng)存在了,最后把值覆蓋掉。這就是 HashMap 定義值不能重復(fù)的代碼實現(xiàn)。

  5. 如果數(shù)據(jù)達(dá)到了閾值,進(jìn)行擴(kuò)容。

問題3: HashMap 怎么擴(kuò)容?

擴(kuò)容條件:

存放完數(shù)據(jù)以后,判斷當(dāng)前容量是否達(dá)到閾值。一旦達(dá)到則需要進(jìn)行擴(kuò)容:

if (++size > threshold)
    resize();

基本過程:

  1. 容量翻倍;
  2. 重新放置元素。

源碼分析(HashMap 不為空的情況下):

HashMap # 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) {
        // 達(dá)到最大值就不處理了
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 1.左移一位進(jìn)行翻倍
        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) {
        // 2.將所有結(jié)點(diǎn)放置在擴(kuò)容表中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 3.鏈表結(jié)點(diǎn)沒有下個元素,找到下標(biāo)直接存放
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 4.樹結(jié)構(gòu)單獨(dú)處理
                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;
                        // 5.運(yùn)算之后為0,直接存放在當(dāng)前下標(biāo)
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 6.不為0,存放到當(dāng)前下標(biāo)+原來長度(oldCap)的位置
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 5.1不需要挪動的頭結(jié)點(diǎn)
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 6.1需要挪動的頭結(jié)點(diǎn),放置在 j + oldCap 下標(biāo)處
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

重點(diǎn)在于:

  • 進(jìn)行遍歷,如果某個元素不為鏈表或樹結(jié)構(gòu),直接存放即可。因為原來它所在的下標(biāo)就一個值,直接放在原位置即可。
  • 某個元素為鏈表,遍歷鏈表中的結(jié)點(diǎn)。將 & 運(yùn)算結(jié)果為 0 串起來,將結(jié)果不為 0 的串起來。最后將為 0 的一串放在原來下標(biāo)位置,不為 0 的一串放在 [原來下標(biāo)+原來長度] 的位置。為 0 的一串不需解釋,放在原來位置。
    不為 0 的打個比方:原來長度是 4,下標(biāo)為 1。數(shù)組擴(kuò)容為兩倍 8,那么需要放置在位置下標(biāo)為 4+1=5 處。這樣就比較均勻地分散了數(shù)據(jù)。

問題4: HashMap 初始容量大小?

默認(rèn)容量是 16:

 /**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

默認(rèn)負(fù)載因子:

 /**
  * The load factor used when none specified in constructor.
  */
 static final float DEFAULT_LOAD_FACTOR = 0.75f;

如果創(chuàng)建 HashMap 時傳入初始容量 k,則初始大小為距離 k 最近的 2 的整數(shù)次方。比如傳入 10,經(jīng)過計算為 16。

/**
 * Returns a power of two size for the given target capacity.
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

二進(jìn)制右移并進(jìn)行位或,結(jié)果會把所有位變?yōu)?1,最后再 +1 得到最接近的 2 的冪數(shù)整數(shù)。

問題5: HashMap 下標(biāo)確認(rèn)過程?

  1. 生成散列值:
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

允許 key 為 null,返回 hash 碼 0。反之,拿到該對象的 hashCode,然后將該 hashCode 的高 16 位與低 16 位進(jìn)行異或(相同為 0 不同為 1)。

進(jìn)行運(yùn)算

為什么散列值這樣生成?

高 16 位與低 16 位進(jìn)行異或的算法叫做 擾動函數(shù),這么做的好處有

  • 盡可能降低了 hash 碰撞;
  • 位或運(yùn)算效率較高;
  • 變相地保留了 hashCode 的高 16 位。

為什么不直接使用 hashCode 作為散列值

先搞清楚下標(biāo)生成規(guī)則:

  1. 將數(shù)組長度 -1 和 hash 值進(jìn)行與運(yùn)算(都想同為 1,否則為 0),就確定了下標(biāo)。
Node<K,V> p;
int n = table.length;
p = tab[(n - 1) & hash];

打個比方,數(shù)組長度為 16,將某個 hash 值和 長度-1 進(jìn)行與運(yùn)算。因為只保留了后幾位,所以得到的結(jié)果肯定不大于數(shù)組長度。

  10100101 11000100 00100101
& 00000000 00000000 00001111
--------------------------------
  00000000 00000000 00000101    //高位全部歸零,只保留末四位

由于下標(biāo)的生成主要在于數(shù)組長度的后幾位,如果只是使用 hashCode 特容易發(fā)生碰撞。使用擾動函數(shù)處理過之后,加大了低位的隨機(jī)性,減少碰撞。

這里也就說明了數(shù)組容量為 2 的整數(shù)冪的原因:要進(jìn)行與運(yùn)算確定下標(biāo)。為 2 的整數(shù)冪,計算時減 1 確保后位全部為 1,這樣與運(yùn)算的時候會有很大的隨機(jī)性。如果不是 2 的整數(shù)冪減 1,后位很多 0、運(yùn)算的時候極易得到相同的下標(biāo),造成效率降低、內(nèi)存浪費(fèi)的后果。

問題6: HashMap 怎么獲取元素?

基本過程:

  1. 根據(jù) key 得到散列值,傳入 key 進(jìn)行查找。
  2. 找到直接返回,找不到遍歷。

HashMap # get()

 public V get(Object key) {
     Node<K,V> e;
     // hash(key) 獲得傳入 key 的 hash 碼
     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;
    // 1. 判空,如果 HashMap 為空直接返回 null
    // 數(shù)組長度和 hash 進(jìn)行與運(yùn)算,得到所查找元素下標(biāo),找不到同樣返回 null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 2. 如果要查找 key 與第一個元素 hash 和 key 都相同,直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 3. 遍歷查找后面的元素,找到就返回。找不到最后會返回 null
        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;
}

HashMap 查詢元素時間復(fù)雜度

  • 根據(jù)上方查找元素的源碼,如果能直接確認(rèn)所在下標(biāo)就是要查詢的元素,直接返回。時間復(fù)雜度為 O(1)。
  • 如果需要遍歷鏈表,時間復(fù)雜度為 O(n)。

以上僅為個人理解。

問題7: HashMap 遍歷原理?

先來看一下 HashMap 遍歷的方式:

HashMap<Object, Object> hashMap = new HashMap();
// 遍歷key
for (Object k : hashMap.keySet()) {
}
// 遍歷元素
for (HashMap.Entry<Object, Object> entry : hashMap.entrySet()) {
}

// 迭代器遍歷key
Set<Object> keySet = hashMap.keySet();
Iterator<Object> iterator = keySet.iterator();
while (iterator.hasNext()){
    Object o = iterator.next();
}
// 迭代器遍歷元素
Set<HashMap.Entry<Object, Object>> keySetI = hashMap.entrySet();
Iterator<HashMap.Entry<Object, Object>> iteratorI = keySetI.iterator();
while (iteratorI.hasNext()){
    HashMap.Entry<Object, Object> entry = iteratorI.next();
}

直接遍歷

hashMap.keySet() 方法會返回一個 KeySet 集合:

HashMap # keySet()

    public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }

使用 for 循環(huán)即可遍歷該 Set 集合,獲取到 Key 的集合。

KeySet 是 HashMap 的一個內(nèi)部類,創(chuàng)建時持有 HashMap 的第一個元素,有了開頭的元素就可以執(zhí)行遍歷了。后面會詳細(xì)記錄它的實現(xiàn)。

迭代器遍歷

keySet.iterator() 方法或獲取到 HashMap 的迭代器:

final class KeySet extends AbstractSet<K> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<K> iterator()     { return new KeyIterator(); }
    public final boolean contains(Object o) { return containsKey(o); }
    public final boolean remove(Object key) {
        return removeNode(hash(key), key, null, false, true) != null;
    }
    public final Spliterator<K> spliterator() {
        return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    ...
}

進(jìn)行遍歷用到迭代器的 next 方法

KeyIterator # next()

final class KeyIterator extends HashIterator
     implements Iterator<K> {
     public final K next() { return nextNode().key; }
 }

nextNode() 方法是父類 HashIterator 的,先看下 HashIterator 的構(gòu)造函數(shù):

abstract class HashIterator {
    Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot
    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        // next 持有第一個元素
        if (t != null && size > 0) { // advance to first entry
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }
    ...
}

注意成員變量 next 持有的 HashMap 的第一個元素的結(jié)點(diǎn),再看 nextNode() 方法:

HashMap.HashIterator # nextNode()

final Node<K,V> nextNode() {
    Node<K,V>[] t;
    // 1. 把 next 結(jié)點(diǎn)復(fù)制給 e,從第一個元素開始查找
    Node<K,V> e = next;
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    if (e == null)
        throw new NoSuchElementException();
     // 2.第一次調(diào)用 e 為初始結(jié)點(diǎn),直接返回,并且 next 成為 e 的下一個結(jié)點(diǎn)。
     // 如果 e 的下一個結(jié)點(diǎn)為 null 并且表不為空,把表下一個元素賦值給 next。
    if ((next = (current = e).next) == null && (t = table) != null) {
        do {} while (index < t.length && (next = t[index++]) == null);
    }
    return e;
}

這樣完成了每一個結(jié)點(diǎn)的遍歷,至于 HashMap.Entry 的遍歷與之類似,不再贅述。

HashMap 刪除實現(xiàn)

HashMap 元素的移除就相對簡單了,直接看源碼。

HashMap # remove

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 判斷數(shù)組是否為空,要移除的元素是否為空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        // key 對上了,node 記錄要移除的元素
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        // 沒對上,說明可能在鏈表或紅黑樹中
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                // 遍歷查找,直到找到了元素或查詢完畢
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)// 樹節(jié)點(diǎn)刪除
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p) // 如果是首節(jié)點(diǎn),直接把下標(biāo)位置指向下一個結(jié)點(diǎn)
                tab[index] = node.next;
            else // 如果不是,直接指向下一個結(jié)點(diǎn),當(dāng)前結(jié)點(diǎn)就刪除了
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

總結(jié)

文中有關(guān)紅黑樹等內(nèi)容暫時沒有分析,等復(fù)習(xí)到數(shù)據(jù)結(jié)構(gòu)再說吧。

參考資料:

一個HashMap跟面試官扯了半個小時
HashMap 源碼詳細(xì)分析(JDK1.8)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容