1. 程式人生 > 實用技巧 >HashMap原理與理解

HashMap原理與理解

本文以 Java 1.8 為基礎進行展開。

一、HashMap的基本結構

  HashMap是基於雜湊表的Map介面的非同步實現。此實現提供所有可選的對映操作,並允許使用null值和null鍵。此類不保證對映的順序,特別是它不保證該順序恆久不變。

  在java程式語言中,最基本的結構就是兩種,一個是陣列,另外一個是模擬指標(引用),所有的資料結構都可以用這兩個基本結構來構造,比如String、ArrayList等,HashMap也不例外。HashMap實際上是一個“連結串列雜湊”的資料結構,即陣列、連結串列、紅黑樹的結合體(Java 1.8引入了紅黑樹結構)。

  陣列的特點:查詢效率高,插入,刪除效率低。

  連結串列的特點:查詢效率低,插入刪除效率高。

  在HashMap底層使用陣列加(連結串列或紅黑樹)的結構完美的解決了陣列和連結串列的問題,使得查詢和插入,刪除的效率都很高。

  其結構模型可參考下圖(借用baidu的兩張圖):

  

二、HashMap的基本屬性、方法

  1.HashMap的成員變數

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //預設的初始容量為16

static final int MAXIMUM_CAPACITY = 1 << 30; //最大的容量為 2 ^ 30

static final
float DEFAULT_LOAD_FACTOR = 0.75f; //預設的載入因子為 0.75f static final int TREEIFY_THRESHOLD = 8; //連結串列長度大於等於8時轉化為紅黑樹 static final int UNTREEIFY_THRESHOLD = 6; //紅黑樹長度小於等於6時轉化為連結串列 static final int MIN_TREEIFY_CAPACITY = 64; //連結串列轉為紅黑樹時要求的table capacity最小容量 transient Node<K,V>[] table; //Node型別的陣列(實現了Entry介面),HashMap的基本組成單元,用來儲存key-value對映
transient Set<Map.Entry<K,V>> entrySet; // transient int size; //HashMap包含key-value對映的數量 final float loadFactor; //載入因子 int threshold; //HashMap的擴容臨界點(容量和載入因子的乘積) transient int modCount; // 每次擴容和更改map結構的計數器
View Code

  計算陣列index的時候,為什麼要用位運算&呢?

  主要是效率問題,位運算(&)效率要比取模運算(%)高很多,主要原因是位運算直接對記憶體資料進行操作,不需要轉成十進位制,因此處理速度非常快。在jdk1.8之前的index計算就是用的取模運算%。

  MAXIMUM_CAPACITY為什麼設定成 1 << 30

  i. HashMap在確定陣列下標Index的時候,採用的是( length-1) & hash的方式。只有當length為2的指數冪,2的n次-1的二進位制表示剛好全為1,這樣&運算確定的index才能分佈均勻,不然如果有一位是0,那

麼與運算結果對應的這一位也永遠是0,那對應的陣列index處就為空,index分佈不均勻了。所以HashMap規定了其容量必須是2的n次方,這樣才能較均勻的分佈元素,hash%length = hash&(length-1)才能成

立。

  ii. 另外,HashMap內部由Node[](Entry[])陣列構成,Java的陣列下標是由Integer表示的。所以對於HashMap來說其最大的容量應該是不超過Integer最大值的一個2的指數冪,而最接近Integer最大值的2的

指數冪就是 1 << 30。此時,HashMap也就無法再繼續擴容。

  DEFAULT_LOAD_FACTOR = 0.75f載入因子

  loadFactor載入因子,是用來衡量 HashMap 滿的程度,表示HashMap的疏密程度,影響hash操作到同一個陣列位置的概率。計算HashMap的實時載入因子的方法為:size/capacity,而不是佔用桶的數量去除以capacity。capacity 是桶的數量,也就是 table 的長度length。當Size>=threshold的時候,那麼就要考慮對陣列的resize(擴容)。當連結串列的值超過8則會轉紅黑樹(jdk 1.8新增)

  loadFactor太大導致查詢元素效率低,太小導致陣列的利用率低,存放的資料會很分散。loadFactor的預設值為0.75f是官方給出的一個比較好的臨界值。

  當HashMap裡面容納的元素已經達到HashMap陣列長度的75%時,表示HashMap太擠了,需要擴容,而擴容這個過程涉及到 rehash、複製資料等操作,非常消耗效能。所以開發中儘量減少擴容的次數,可

以通過建立HashMap集合物件時指定初始容量來儘量避免。

  同時在HashMap的構造器中可以指定loadFactor:

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
View Code

  TREEIFY_THRESHOLD = 8; UNTREEIFY_THRESHOLD = 6;

  為什麼Map桶中節點個數大於等於8轉為紅黑樹?桶中連結串列元素個數小於等於6時,樹結構還原成連結串列?

  解釋1.參考自:https://www.cnblogs.com/xc-chejj/p/10825676.html

  因為紅黑樹的平均查詢長度是log(n),長度為8的時候,平均查詢長度為3,如果繼續使用連結串列,平均查詢長度為8/2=4,這才有轉換為樹的必

要。連結串列長度如果是小於等於6,6/2=3,雖然速度也很快的,但是轉化為樹結構和生成樹的時間並不會太短。

  還有選擇6和8,中間有個差值7可以有效防止連結串列和樹頻繁轉換。假設一下,如果設計成連結串列個數超過8則連結串列轉換成樹結構,連結串列個數小於8則樹結構轉換成連結串列,如果一個HashMap不停的插入、刪除元

素,連結串列個數在8左右徘徊,就會頻繁的發生樹轉連結串列、連結串列轉樹,效率會很低。

  解釋2. 參考自:https://www.cnblogs.com/coding-996/p/12468618.html

  根據官方原始碼的註釋可以看到:

threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:
     *
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million
View Code

  大概意思就是說,在理想情況下,使用隨機雜湊碼,節點出現在hash桶中的頻率遵循泊松分佈,同時給出了桶中元素個數和概率的對照表。從上面的表中可以看到當桶中元素到達8個的時候,概率已經變得非

常小,也就是說用0.75作為載入因子,每個碰撞位置的連結串列長度超過8個的概率小於一千萬分之一。即載入因子為0.75,同一個桶中出現8個元素然後轉化為紅黑樹的概率小於1000萬分之一。

  MIN_TREEIFY_CAPACITY = 64

  當HashMap桶的容量超過這個值時才能進行樹形化 ,否則會先選擇擴容,而不是樹形化。為了避免進行擴容、樹形化選擇的衝突,這個值不能小於 4 * TREEIFY_THRESHOLD。

  Node<K,V>[] table

  在jdk1.7中,Entry陣列是HashMap的基本組成單元:

View Code

  jdk1.8中,HashMap基本單元由Node陣列組成,Node陣列實現了Entry介面:

View Code

  當然,還有jdk1.8 引入的紅TreeNode靜態內部類:

View Code

  Set<Map.Entry<K,V>> entrySet

  HashMap中所有key-value對映的集合,儲存了所有的key-value鍵值對。可通過entrySet() 方法獲取:

 public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
 }
View Code

  粗略一看,發現HashMap中並沒有主動地去維護entrySet,比如put的時候去存值或者呼叫entrySet()去維護值,那entryset的值從哪而來呢?具體在後面的entrySet()方法中說明。

  2.HashMap的常用方法

  (1)HashMap的主要構造器

  i. HashMap():構建一個初始容量為 16,負載因子為 0.75 的 HashMap

  View Code

  ii. HashMap(int initialCapacity):設定了初始容量initialCapacity,但方法內部會基於initialCapacity重新計算,得到一個不小於initialCapacity的最小的2的指數冪,並將其作為threshold(具體計算邏輯見下面的tableSizeFor() 方法和 put() 方法),同時設定負載因子為 0.75

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

    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;
    }
View Code

  iii. HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負載因子建立一個 HashMap。同樣,方法內部也基於initialCapacity重新計算了threshold

  原始碼見ii 中所述方法。

  (2)tableSizeFor() 方法

  原始碼見上述ii 中程式碼。該方法對給定初始容量initialCapacity進行初始化,將其修改為不小於initialCapacity的、最小的2的n次冪,以此來保證HashMap的容量只會是2的冪。也就是說如果傳入的引數為x,那麼呼叫該方法應該返回一個大於等於x的最小的2的冪。

                                // 10000000 00000000 00000000 00000000
        n |= n >>> 1;       // 11000000 00000000 00000000 00000000
        n |= n >>> 2;       // 11110000 00000000 00000000 00000000
        n |= n >>> 4;       // 11111111 00000000 00000000 00000000
        n |= n >>> 8;       // 11111111 11111111 00000000 00000000
        n |= n >>> 16;      // 11111111 11111111 11111111 11111111
    
View Code

  其實,我們只需要關心從左邊數第一個不為零的位,其他的位是1是0都不重要。n |= n >>> 1之後可以確保,從左邊數第一個不為零的位開始前兩位都是1,n |= n >>> 2之後可以確保前四位都是1。以此類推,最後得到的就是從第一個不為零的位開始全為1的數,再將這個數+1就可以得到大於等於cap這個變數的最小的2的冪了。

  移位5次就停止是因為HashMap最大容量是1<<30 (2的30次方),是一個int型資料。在java中int型是4個位元組也就是32位,除去符號位就是31位,進行5次移位之後已經足以保證31位全為1了。

  參考自:https://blog.csdn.net/qq_41046325/article/details/88626353 

  值得注意的是,上面的原始碼中,是將tableForSize的值賦值給了threshold, 那為何說是我們初始化容量(capacity)的大小為該值呢?可以下面的put()方法。

  (3)put()方法

  先解釋一下上面剛剛提出的問題。在第一次向map新增資料時,呼叫:

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

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;   //注意這一行程式碼
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
           ...
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
View Code

  注意上面,putVal會判斷table是否為null

  if ((tab = table) == null || (n = tab.length) == 0)

  如果為null,則呼叫resize方法:

  n = (tab = resize()).length;

  而resize()方法(原始碼見下文)實際上就是將之前設定的threshold作為了初始化的容量大小。

  參考自:https://www.cnblogs.com/shuhe-nd/p/12011269.html

  為了邏輯清晰、簡潔易懂,在此參考另一篇部落格中的一張圖,put() 方法的邏輯可以用下圖來表示:

  

  上圖參考自:https://www.cnblogs.com/one-apple-pie/p/10473682.html  

  (4)resize()方法

  當hashmap中的元素越來越多的時候,碰撞的機率也就越來越高(因為陣列的長度是固定的),所以為了提高查詢的效率,就要對hashmap的陣列進行擴容,陣列擴容這個操作也會出現在ArrayList中,所以這是一個通用的操作,很多人對它的效能表示過懷疑,不過想想我們的“均攤”原理,就釋然了,而在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) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            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) {
            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
                        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;
                        }
                    }
                }
            }
        }
        return newTab;
    }
View Code

  resize的大致邏輯:

  i. 校驗和擴容:校驗capacity 和threshold,重新計算這兩個引數,並新建一個Entry空陣列,長度是原來的兩倍。

  ii. reHash :遍歷原來的Entry陣列,把所有的資料重新Hash到新的陣列。

  那麼hashmap什麼時候進行擴容呢?當hashmap中的元素個數超過陣列大小*loadFactor時,就會進行陣列擴容,loadFactor的預設值為0.75,也就是說,預設情況下,陣列大小為16,那麼當hashmap中元素個數超過16*0.75=12的時候,就把陣列的大小擴充套件為2*16=32,即擴大一倍,然後重新計算每個元素在陣列中的位置,而這是一個非常消耗效能的操作,所以如果我們已經預知hashmap中元素的個數,那麼預設元素的個數能夠有效的提高hashmap的效能。比如說,我們有1000個元素new HashMap(1000), 但是理論上來講new HashMap(1024)更合適,不過即使是1000,hashmap也自動會將其設定為1024。 但是new HashMap(1024)還不是更合適的,因為0.75*1000 < 1000, 也就是說為了讓0.75 * size > 1000, 我們必須這樣new HashMap(2048)才最合適,既考慮了&的問題,也避免了resize的問題。

  上面參考自:https://www.cnblogs.com/yuanblog/p/4441017.html

  為什麼要重新Hash呢,直接複製過去不是更快捷方便嗎?

  • 因為擴容以後的陣列長度變了,index = HashCode(key)&(length - 1),擴容後的length和之前不一樣了,變為原來的2倍,重新Hash算出來的index值肯定也不一樣,而且重新計算後,會使元素更加均勻的分佈在HashMap表中,如果直接複製的話,那麼資料肯定都堆在一起了,擴容的意義就削弱了。

  (5)entrySet()方法

  i. 先看一段常用的遍歷程式碼:

View Code

  上面的遍歷方式可以遍歷HashMap中所有的key-value鍵值對,為什麼entrySet()會返回值呢?繼續看原始碼:

View Code View Code
    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }
View Code
    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;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }
View Code

  可以看到,上面entrySet() 方法中在entrySet為空時,會初始化一個EntrySet類。而該類中重寫了一個方法iterator(),並且在方法內部呼叫了EntryIterator 的構造方法。EntryIterator 類又繼承了HashIterator類,所以會預設呼叫該類的構造方法。在HashIterator 的構造方法中,會初始化一個next變數,構造初期會從0開始找有值(不為空)的索引位置,找到後將這個Node賦值給next;然後要遍歷的時候呼叫了EntryIterator的 next() 方法,即呼叫了HashIterator的nextNode() 方法,這個方法會一直遍歷找到下一個有值(不為空)的索引位置。如此反覆。

  所以,HashMap在put 的時候維護了Node<K,V>[] table,然後在entrySet() 的時候會遍歷這個table,從而獲得所有的key-value鍵值對。

  參考自:https://www.cnblogs.com/javammc/p/7631597.html

  另外,關於entrySet 值初始化的問題,可能跟預設呼叫toString()方法有關,可以參考下面的討論:

  https://www.cnblogs.com/allmignt/p/12353732.htmlhttps://segmentfault.com/q/1010000012304833

  ii. 遍歷方法對比

  (i) 通過HashMap.entrySet()獲得鍵值對的Set集合,如上面 i 中所述方式。

  (ii)通過HashMap.keySet()獲得鍵的Set集合(iterator或foreach)

        Map<String, String> map = new HashMap<String, String>();
        map.put("1", "11");
        map.put("2", "22");
        map.put("3", "33");
        // 鍵和值
        String key = null;
        String value = null;
        // 獲取鍵集合的迭代器
        Iterator it = map.keySet().iterator();
        while (it.hasNext()) {
            key = (String) it.next();
            value = (String) map.get(key);
            System.out.println("key:" + key + "---" + "value:" + value);
        }
View Code

  (iii)通過HashMap.values()得到“值”的集合

        Map<String, String> map = new HashMap<String, String>();
        map.put("1", "11");
        map.put("2", "22");
        map.put("3", "33");
        //
        String value = null;
        // 獲取值集合的迭代器
        Iterator it = map.values().iterator();
        while (it.hasNext()) {
            value = (String) it.next();
            System.out.println("value:" + value);
        }
View Code

  上述(i)與(ii)的不同之處在於獲取到相應集合之後,在遍歷的時候時間複雜度不同:(i)在遍歷時通過iterator獲取下一個鍵值對,時間複雜度為O(1),而(ii)呼叫get()方法則又會進行一次遍歷。因此方式(i)的效能要更優於方式(ii),尤其是在map容量比較大的時候。  

  (iiii) Lambda表示式

  (iiiii) stream API

  連結的文中對HashMap的遍歷方式列舉比較全面(包括Lambda表示式、stream API),可以進行學習參考:https://blog.csdn.net/sufu1065/article/details/105852634?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

  三、執行緒安全 

  1. 連結串列頭插法、尾插法

  舉個例子,現在採用多執行緒往一個容量大小為2的HashMap中put 值:A = 1 ,B = 2,C = 3 ,負載因子是0.75。

  正常單執行緒的情況下,在put第二個值的時候就會進行resize。對於多執行緒而言,假如各自都安全地完成了資料的插入(但還沒有觸發擴容),此時A->B->C,狀態如下所示:

​  

​  現在進行擴容。如果使用單鏈表的頭插法,同一index位置上的新元素總會被放到連結串列的頭部,假如某一執行緒這時剛好把 B 重新hash到了A原來的位置,如圖:

  

​ 由於是多執行緒同時操作,當所有執行緒都執行完畢以後,就可能會出現這樣的情況:

​ 此時居然出現了環狀的連結串列結構,如果這個時候去取值,就會出錯——InfiniteLoop(死迴圈)。

  Java7在多執行緒操作HashMap時,採用了頭插法,在轉移過程中修改了原連結串列中節點的引用關係(互相顛倒),很有可能引起死迴圈;Java8採用尾插法,就不會引起死迴圈,原因是擴容前後(如果)仍然位於同一連結串列上的元素,他們的相對引用順序不會顛倒。

  總之,Java7如果多個執行緒同時觸發擴容,在移動節點時可能會導致一個連結串列中的2個節點相互引用,從而生成環連結串列。

  四、與Jdk1.7對比

  1.HashMap基本構成

  參考上面的:Node<K,V>[] table部分

  2. 資料插入連結串列時,JDK8以前是頭插法,JDK8是尾插法

  參考三、執行緒安全

  3. JDK8引入了紅黑樹(相對平衡的二叉搜尋樹),提高了查詢效率。

  紅黑樹可以參考:https://www.cnblogs.com/LiaHon/p/11203229.html

  五、小結:

  1. 擴容是一個特別耗效能的操作,所以在使用HashMap的時候,估算map的大小,初始化的時候給一個大致的數值,避免map進行頻繁的擴容。

  2. 負載因子是可以修改的,也可以大於1,但是建議不要輕易修改,除非情況非常特殊。

  3. HashMap是執行緒不安全的,不要在併發的環境中同時操作HashMap,建議使用ConcurrentHashMap。