Java 容器 - 一文詳解HashMap

Map 類集合

Java Map類集合,與Collections類集合存在很大不一樣。它是與Collection 類平級的一個接口。java

在集合框架中,經過部分視圖方法這一根 微弱的線聯繫起來。node

(在以後的分享中,咱們會討論到Collections 框架的內容)面試

Map類集合中的存儲單位是K-V鍵值對,就是 使用必定的哈希算法造成一組比較均勻的哈希值做爲Key,Value值掛在Key上。算法

Map類 的特色:bootstrap

  • 沒有重複的Key,能夠具備多個重複的Value
  • Value能夠是List/Map/Set對象
  • KV是否容許爲null,以實現類的約束爲準
Map集合類 Key Value Super JDK 說明
Hashtable 不容許爲 null 不容許爲 null Dictionary 1.0 (過期)線程安全類
ConcurrentHashMap 不容許爲 null 不容許爲 null AbstractMap 1.5 鎖分段技術或CAS(JDK8 及以上)
TreeMap 不容許爲 null 容許爲 null AbstractMap 1.2 線程不安全(有序)
HashMap 容許爲 null 容許爲 null AbstractMap 1.2 線程不安全(resize 死鏈問題)
從jdk1.0-1.5,這幾個重點KV集合類,見證了Java語言成爲工業級語言的成長曆程。

知識點:數組

  1. Map 類 特有的三個方法是keySet()values()entrySet(),其中values()方法返回的視圖的集合實現類是Values extends AbstractCollection<V>,沒有實現add操做,實現了remove/clear等相關操做,調用add方法時會拋出異常。
  2. 在大多數狀況下,直接使用ConcurrentHashMap替代HashMap沒有任何問題,性能上面差異不大,且線程安全。
  3. 任何Map類集合中,都要儘可能避免KV設置爲null值。
  4. Hashtable - HashMap - ConcurrentHashMap 之間的關係 大體至關於 Vector - ArrayList - CopyOnWriteArrayList 之間的關係,固然HashMap 和 ConcurrentHashMap之間性能差距更小。

1、hashCode()

哈希算法 哈希值

在Object 類中,hashCode()方法是一個被native修飾的類,JavaDoc中描述的是返回該對象的哈希值。安全

那麼哈希值這個返回值是有什麼做用呢?數據結構

主要是保證基於散列的集合,如HashSet、HashMap以及HashTable等,在插入元素時保證元素不可重複,同時爲了提升元素的插入刪除便利效率而設計;主要是爲了查找的便捷性而存在。

拿Set進行舉例,app

衆所周知,Set集合是不能重複,若是每次添加數據都拿新元素去和集合內部元素進行逐一地equal()比較,那麼插入十萬條數據的效率能夠說是很是低的。框架

因此在添加數據的時候就出現了哈希表的應用,哈希算法也稱之爲散列算法,當添加一個值的時候,先去計算出它的哈希值,根據算出的哈希值將數據插入指定位置。這樣的話就避免了一直去使用equal()比較的效率問題。

具體表如今:

  • 若是指定位置爲空,則直接添加
  • 若是指定位置不爲空,調用equal() 判斷兩個元素是否相同,若是相同則不存儲

上述第二種狀況中,若是兩個元素不相同,可是hashCode()相同,那就是發生了咱們所謂的哈希碰撞。

哈希碰撞的機率取決於hashCode()計算方式和空間容量的大小。

這種狀況下,會在相同的位置,建立一個鏈表,把key值相同的元素存放到鏈表中。

在HashMap中就是使用拉鍊法來解決hashCode衝突。

總結

hashCode是一個對象的標識,Java中對象的hashCode是一個int類型值。經過hashCode來指定數組的索引能夠快速定位到要找的對象在數組中的位置,以後再遍歷鏈表找到對應值,理想狀況下時間複雜度爲O(1),而且不一樣對象能夠擁有相同的hashCode。

HashMap 底層實現

帶着問題

  1. HashMap 的長度爲何默認初始長度是16,而且每次resize()的時候,長度必須是2的冪次方?
  2. 你熟悉HashMap的擴容機制嗎?
  3. 你熟悉HashMap的死鏈問題嗎?
  4. Java 7 和 Java 8 HashMap有哪些差異?
  5. 爲何Java 8以後,HashMap、ConcurrentHashMap要引入紅黑樹?

0. 簡介

  1. HashMap 基於哈希表的Map接口實現的,是以Key-Value存儲形式存在;
  2. 非線程安全;
  3. key value均可覺得null;
  4. HashMap中的映射不是有序的;
  5. 在 JDK1.8 中,HashMap 是由 數組+鏈表+紅黑樹構成,新增了紅黑樹做爲底層數據結構;
  6. 當一個哈希桶存儲的鏈表長度大於8 會將鏈表轉換成紅黑樹,小於6時則從紅黑樹轉換成鏈表;
  7. 1.8以前和1.8及之後的源碼,差異較大

1. 存儲結構

在 JDK1.8 中,HashMap 是由 數組+鏈表+紅黑樹構成,新增了紅黑樹做爲底層數據結構。

經過哈希來確認到數組的位置,若是發生哈希碰撞就以鏈表的形式存儲 ,可是這樣若是鏈表過長來的話,HashMap會把這個鏈表轉換成紅黑樹來存儲,閾值爲8。

下面是HashMap的結構圖:

2. 重要屬性

2.1 table

/**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;

在JDK1.8中咱們瞭解到HashMap是由數組加鏈表加紅黑樹來組成的結構其中table就是HashMap中的數組。

2.2 size

/**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

HashMap中 鍵值對存儲數量。

2.3 loadFactor

/**
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor;

負載因子。負載因子是權衡資源利用率與分配空間的係數。當元素總量 > 數組長度 * 負載因子 時會進行擴容操做。

2.4 threshold

/**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;

擴容閾值。threshold = 數組長度 * 負載因子。超事後執行擴容操做。

2.5 TREEIFY_THRESHOLD/UNTREEIFY_THRESHOLD

/**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

樹形化閾值。當一個哈希桶存儲的鏈表長度大於8 會將鏈表轉換成紅黑樹,小於6時則從紅黑樹轉換成鏈表。

3. 增長元素

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

3.1 hash()

能夠看到實際執行添加元素的是putVal()操做,在執行putVal()以前,先是對key執行了hash()方法,讓咱們看下里面作了什麼

static final int hash(Object key) {
        int h;
        // key.hashCode():返回散列值也就是hashcode
        // ^ :按位異或
        // >>>:無符號右移,忽略符號位,空位都以0補齊
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

key==null說明,HashMap中是支持key爲null的狀況的。

一樣的方法在Hashstable中是直接用key來獲取hashCode,沒有key==null的判斷,因此Hashstable是不支持key爲null的。

再回來講這個hash()方法。這個方法用專業術語來稱呼就叫作擾動函數

使用hash()也就是擾動函數,是爲了防止一些實現比較差的hashCode()方法。換句話來講,就是爲了減小哈希碰撞

JDK 1.8 的 hash方法 相比於 JDK 1.7 hash 方法更加簡化,可是原理不變。咱們再看下JDK1.7中是怎麼作的。

// code in JDK1.7
        static int hash(int h) {
            // This function ensures that hashCodes that differ only by
            // constant multiples at each bit position have a bounded
            // number of collisions (approximately 8 at default load factor).
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }

相比於 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能會稍差一點點,由於畢竟擾動了 4 次。

知識延伸外鏈: JDK 源碼中 HashMap 的 hash 方法原理是什麼? - 胖君的回答 - 知乎
https://www.zhihu.com/questio...

3.2 putVal()

再來看真正執行增長元素操做的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;
        // 當數組爲空或長度爲0,初始化數組容量(resize() 方法是初始化或者擴容用的)
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 計算數組下標 i = (n-1) & hash
        // 若是這個位置沒有元素,則直接建立Node並存值
        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))))
                // hash值、key值相等,用e變量獲取到當前位置這個元素的引用,後面用於替換已有的值
                e = p;
            else if (p instanceof TreeNode)
                // 當前是以紅黑樹方式存儲,執行其特有的putVal方法 -- putTreeVal
                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)
                    // onlyIfAbsent 若是爲true - 不覆蓋已存在的值
                    // 把新值賦值進去
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 記錄修改次數
        ++modCount;
        // 判斷元素數量是否超過閾值 超過則擴容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

3.3 HashMap 的長度爲何默認初始長度是16,而且每次resize()的時候,長度必須是2的冪次方?

這是一個常見的面試題。這個問題描述的設計,實際上爲了服務於從Key映射到數組下標index的Hash算法。

前面提到了,咱們爲了讓HashMap存儲高效,應該儘可能減小哈希碰撞,也就是說,應該讓元素分配得儘量均勻。

Hash 值的範圍值-21474836482147483647,先後加起來大概40億的映射空間,只要哈希函數映射得比較均勻鬆散,通常應用是很難出現碰撞的。但問題是一個40億長度的數組,內存是放不下的。因此這個散列值是不能直接拿來用的。

因此才須要一個映射的算法。這個計算方式就是3.2中有出現的(n - 1) & hash

咱們來進一步演示一下這個算法:

  1. 假設有一個key="book"
  2. 計算book的hashCode值,結果爲十進制的3029737,二進制的101110001110101110 1001。
  3. 假定HashMap長度是默認的16,計算Length-1的結果爲十進制的15,二進制的1111。
  4. 把以上兩個結果作與運算,101110001110101110 1001 & 1111 = 1001,十進制是9,因此 index=9。

經過這種與運算的方式,可以和取模運算同樣的效果hashCode % length,在上述例子中就是3029737 % 16=9

而且經過位運算的方式大大提升了性能。

可能到這裏,你仍是不知道爲何長度必須是2的冪次方,也是由於這種位運算的方法。

長度16或者其餘2的冪,Length-1的值是全部二進制位全爲1,這種狀況下,index的結果等同於HashCode後幾位的值。只要輸入的HashCode自己分佈均勻,Hash算法的結果就是均勻的。若是HashMap的長度不是2的冪次方,會出現某些index永遠不會出現的狀況,這個顯然不符合均勻分佈的原則和指望。因此在源碼裏面一直都在強調power-of-two expansionsize must be power of two

另外,HashMap 構造函數容許用戶傳入的容量不是 2 的 n 次方,由於它能夠自動地將傳入的容量轉換爲 2 的 n 次方。

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

4. HashMap 擴容

接下來咱們來說講HashMap擴容相關的知識。

4.1 擴容

HashMap的初始長度是16,假設HashMap中的鍵值對一直在增長,可是table數組容量一直不變,那麼就會發生哈希碰撞,查找的效率確定會愈來愈低。因此當鍵值對數量超過某個閾值的時候,HashMap就會執行擴容操做。

那麼擴容的閾值是怎麼計算的呢?

閾值 = 數組長度 * 負載因子

threshold = capacity * loadFactor

每次擴容後,threshold 加倍

上述計算就出如今resize()方法中。下面會詳細解析這個方法。咱們先繼續往下講。

loadFactor這個參數,咱們以前提到過,負載因子是權衡資源利用率與分配空間的係數。至於爲何是0.75呢?這個實際上就是一個做者認爲比較好的權衡,固然你也能夠經過構造方法手動設置負載因子 。public HashMap(int initialCapacity, float loadFactor) {...)

接下去再來到這裏的主角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;
            }
            // newCap 變成原來的 兩倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 執行擴容操做,新閾值 = 舊閾值 * 2
                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引用的數組
        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
                        ....
                    }
                }
            }
        }
        return newTab;
    }
// 鏈表的處理  這個鏈表處理實際上很是的巧妙
                        // 定義了兩條鏈
                        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;
                        }

上述代碼紅黑樹和鏈表的處理不知道你們看懂了沒有,我反正在第一次看的時候有點暈乎。可是理解了以後有感受很是的巧妙。

拿鏈表處理打比方,它乾的就是把在遍歷舊的table數組的時候,把該位置的鏈表分紅high鏈表和low鏈表。具體是什麼意思呢?看下下面的舉例。

  1. 有一個size爲16的HashMap。有A/B/C/D/E/F六個元素,其中A/B/C的Hash值爲5,D/E/F的Hash值爲21,咱們知道計算數組下標的方法是與運算(效果至關於取模運算),這樣計算出來,A/B/C/D/E/F的index = 5,都會被存在index=5的位置上中。
  2. 假設它們是依次插入,那麼在index爲5的位置上,就會有A->B->C->D->E->F這樣一個鏈表。
  3. 當這個HashMap要進行擴容的時候,此時咱們有舊數組oldTable[],容量爲16,新數組newTable[],容量爲32(擴容數組容量加倍)。
  4. 當遍歷到舊數組index=5的位置的時候,進入到上面提到的鏈表處理的代碼段中,對鏈表上的元素進行Hash & oldCapacity的操做,Hash值爲5的A/B/C計算以後爲0,被分到了low鏈表,Hash爲21的D/E/F被分到了high鏈表。
  5. 而後把low鏈表放入新數組的index=5的位置,把high鏈表放入到新數組的index=5+16=21的位置。

紅黑樹相關的操做雖然代碼不一樣,可是實際上要乾的事情是同樣的。就是把相同位置的不一樣Hash大小的鏈表元素在新table數組中進行分離。但願講到這裏你能聽懂。

4.2 HashMap 死鏈問題

Java7的HashMap會存在死循環的問題,主要緣由就在於,Java7中,HashMap擴容轉移後,先後鏈表順序倒置,在轉移過程當中其餘線程修改了原來鏈表中節點的引用關係,致使在某Hash桶位置造成了環形鏈表,此時get(key),若是key不存在於這個HashMap且key的Hash結果等於那個造成了循環鏈表的Hash位置,那麼程序就會進入死循環

Java8在一樣的前提下並不會引發死循環,緣由是Java8擴容轉移後先後鏈表順序不變,保持以前節點的引用關係

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    Entry[] newTable = new Entry[newCapacity];
    // JDK8 移出了hashSeed計算,由於計算時會調用Random.nextInt(),存在性能問題
    // 很重要的transfer()
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    // 在此步驟完成以前,舊錶上依然能夠進行元素的增長操做,這就是對象丟失緣由之一
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
// 寥寥幾行,卻極爲重要
void transfer(Entry[] newTable, boolean rehash) {
    // newCapacity 是舊錶的兩倍,這個擴容大小
    int newCapacity = newTable.length;
    // 使用foreach 方式遍歷整個數組下標
    for (Entry<K,V> e : table) {
        // 若是在這個slot上面存在元素,則開始遍歷上面的鏈表,知道e==null,退出循環
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            // 當前元素老是直接放在數組下標的slot上,而不是放在鏈表的最後
            // 倒序插入新表
            // 這裏是造成死鏈的關鍵步驟
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}
延伸閱讀。

https://www.yuque.com/docs/sh...

5. Java 8 與 Java 7對比

  1. 發生hash衝突時,Java7會在鏈表頭部插入,Java8會在鏈表尾部插入
  2. 擴容後轉移數據,Java7轉移先後鏈表順序會倒置,Java8仍是保持原來的順序
  3. 引入紅黑樹的Java8極大程度地優化了HashMap的性能‘
  4. put 操做達到閾值時,Java7中是先擴容再新增元素,Java8是先新增元素再擴容;
  5. Java 8 改進了 hash() 擾動函數,提升了性能

6. 爲何要使用紅黑樹?

不少人可能都會答上一句,爲了提升查找性能,但更確切地來講的話,採用紅黑樹的方法是爲了提升在極端哈希衝突的狀況下提升HashMap的性能。

極端哈希衝突的狀況下,去測量Java7和Java8版本的HashMap的查詢性能差距。

Java 7的結果是能夠預期的。 HashMap.get()的性能損耗與HashMap自己的大小成比例增加。 因爲全部鍵值對都在一個巨大的鏈表中的同一個桶中,查找一個條目須要平均遍歷一半這樣的列表(大小爲n)。 所以O(n)複雜性在圖上可視化。

與此相對的是Java8,性能提升了不少,發生災難性哈希衝突的狀況下,在JDK 8上執行的相同基準測試會產生O(logn)最差狀況下的性能。

關於此處的算法優化實際上在 JEP-180中有描述到,

另外若是Key對象若是不是Comparable的話,那麼發生重大哈希衝突時,插入和刪除元素的效率會變不好。(由於底層實現時紅黑樹,須要經過compare方法去肯定順序)

當HashMap想要爲一個鍵找到對應的位置時,它會首先檢查新鍵和當前檢索到的鍵之間是否能夠比較(也就是實現了Comparable接口)。若是不能比較,它就會經過調用tieBreakOrder(Objecta,Object b) 方法來對它們進行比較。這個方法首先會比較兩個鍵對象的類名,若是相等再調用System.identityHashCode 方法進行比較。這整個過程對於咱們要插入的500000個元素來講是很耗時的。另外一種狀況是,若是鍵對象是可比較的,整個流程就會簡化不少。由於鍵對象自身定義瞭如何與其它鍵對象進行比較,就沒有必要再調用其餘的方法,因此整個插入或查找的過程就會快不少。值得一提的是,在兩個可比的鍵相等時(compareTo 方法返回 0)的狀況下,仍然會調用tieBreakOrder 方法。

又可能會有人說了,哪有這麼極端的哈希衝突?

這個其實是一個安全性的考慮,雖然在正常狀況下不多有可能發生不少衝突。可是想象一下,若是Key來自不受信任的來源(例如從客戶端收到的HTTP頭名稱),那麼就有可能收到僞造key值,而且這種作法不難,由於哈希算法是你們都知道的,假設有人有心去僞造相同的哈希值的key值,那麼你的HashMap中就會出現上述這種極端哈希衝突的狀況。 如今,若是你去對這個HashMap執行屢次的查詢請求,就會發現程序執行查詢的效率會變得很慢,cpu佔用率很高,程序甚至會拒絕對外提供服務。

延伸外鏈: https://www.yuque.com/docs/sh...
相關文章
相關標籤/搜索