HashMap (JDK1.8) 分析

1、HashMap(JDK1.8)

一、基本知識、數據結構

(1)時間複雜度:用來衡量算法的運行時間。
  參考:https://blog.csdn.net/qq_41523096/article/details/82142747node

(2)數組:採用一段連續的存儲空間來存儲數據。查找方便,增刪麻煩。面試

(3)鏈表:採用一段不連續的存儲空間存儲數據,每一個數據中都存有指向下一條數據的指針。即 n 個節點離散分配,彼此經過指針相連,每一個節點只有一個前驅節點,每一個節點只有一個後續節點。增刪方便,查找麻煩,算法

(4)紅黑樹:一種自平衡的二叉查找樹,時間複雜度 O(log n)。segmentfault

(5)散列表、哈希表:結合數組 與 鏈表的優勢。經過 散列函數 計算 key,並將其映射到 散列表的 某個位置(連續的存儲空間)。對於相同的 hash 值(產生 hash 衝突),一般採用 拉鍊法來解決。簡單地講,就是將 hash(key) 獲得的結果 做爲 數組的下標,若多個key 的 hash(key) 相同,那麼在當前數組下標的位置創建一個鏈表來保存數據。數組

(6)HashMap:基於 哈希表的 Map 接口的非同步實現(即線程不安全),提供全部可選的映射操做。底層採用 數組 + 鏈表 + 紅黑樹的形式,容許 null 的 Key 以及 null 的 Value。不保證映射的順序且不保證順序恆久不變。安全

二、HashMap JDK1.8 底層數據結構

(1)採用 數組 + 鏈表的形式。
  HashMap 採用 Node 數組來存儲 key-value 鍵值對,且數組中的每一個 Node 其實是一個單向的鏈表,內部存儲下一個 Node 實體的指針。數據結構

 

 

transient Node<K,V>[] table;

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

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

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

 

(2)當前數組長度大於某個閾值(默認爲 64),且鏈表長度大於某個閾值(默認爲 8)時,鏈表會轉爲 紅黑樹。多線程

 

 

 

2、HashMap JDK1.8 源碼分析

一、基本常量、成員變量

/**
* 初始數組容量,必須爲 2 的整數次冪。默認爲 2^4 = 16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

/**
* 最大數組容量, 默認爲 2^30。
*/
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
* 負載因子,默認爲 0.75。
* 用於計算 HashMap 容量。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
* 樹化的第一個條件:
* 鏈表轉紅黑樹的閾值,默認爲 8。
* 即鏈表長度大於等於 8 時,當前鏈表會轉爲紅黑樹進行存儲。
*/
static final int TREEIFY_THRESHOLD = 8;

/**
* 紅黑樹轉鏈表的閾值,默認爲 6。
* 即紅黑樹節點小於等於 6 時,當前紅黑樹會轉爲鏈表進行存儲。
*/
static final int UNTREEIFY_THRESHOLD = 6;

/**
* 樹化的第二個條件:
* 樹化最小容量,默認爲 64。
* 當前數組長度大於等於 64 時,才能夠進行 鏈表轉紅黑樹。
*/
static final int MIN_TREEIFY_CAPACITY = 64

/**
* 數組,用於存儲 Node<K, V> 鏈表
*/
transient Node<K,V>[] table;

/**
* 用於存儲 Node<K, V> 的總個數
*/
transient int size;

/**
* 數組長度閾值,當超過該值後,會調整數組的長度。通常經過 capacity * load factor 計算
*/
int threshold;

/**
* 負載因子,用於計算閾值,默認爲 0.75
*/
final float loadFactor;

/**
* 用於快速失敗(fail-fast)機制,當對象結構被修改後會改變。
*/
transient int modCount;

 

二、核心構造方法

(1)源碼:併發

/**
* 經常使用無參構造方法,以默認值構造 HashMap。
*/
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}


/**
 * HashMap 核心構造方法,根據 初始化容量 以及 負載因子建立 HashMap.
 * @param  initialCapacity 初始化容量
 * @param  loadFactor      負載因子
 * @throws IllegalArgumentException 非法數據異常
 */
public HashMap(int initialCapacity, float loadFactor) {
    // 若是初始化容量 小於 0 ,則會拋出 非法數據 異常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    
    // 若是初始化容量 大於 最大容量值,則給其賦值爲最大值
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
        
    // 若負載因子小於 0 或者 不合法, 拋出 非法數據異常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    
    // 若上述條件均成立,則保存 負載因子的值
    this.loadFactor = loadFactor;
    
    // 若上述條件均成立,則保存 數組長度的閾值(2的整數次冪)。
    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;
}

 

(2)分析:
  上例的構造函數,根據 初始化容量以及 負載因子去建立 HashMap,沒有去 實例化 Node 數組,數組的實例化 須要在 put 方法裏實現。
  數組長度閾值 經過 tableSizeFor() 方法實現,能返回一個比給定容量大的 且 最小的 2 的次冪的數。好比 initialCapacity = 21, tableSizeFor() 返回的結果爲 32。app

三、hash(key)

  用於計算 key 的 hash 值。
(1)源碼:

/**
* 計算 key 的 hash 值的方法
*/
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// Node 數組
transient Node<K,V>[] table;

// 獲取某個 key 所在位置時,經過 (table.length - 1) & hash(key) 去計算數組下標
table[(table.length - 1) & hash(key)]

 

(2)分析
  採用 高 16 位 與 低 16 位 異或,而後再進行移位運算。主要是爲了減小衝突。

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

(length - 1) & hash(key)

【舉例:】
    假設某個值通過 hashCode 計算後爲:
        1111 0101 1010 0101 1101 1110 0000 0000
    數組長度爲 16,那麼 length -1 = 15,以下:
        0000 0000 0000 0000 0000 0000 0000 1111
    此時進行 (length - 1) & hash(key) 操做後,
        1111 0101 1010 0101 1101 1110 0000 0000
        &
        0000 0000 0000 0000 0000 0000 0000 1111
        =
        0000 0000 0000 0000 0000 0000 0000 0000
    即只要 hashCode 計算出的值最後四位爲0,獲得的結果就必定爲 0,此時衝突會大大提升。
    
    採用 高16位 與 低16位 異或,計算爲:
        1111 0101 1010 0101 1101 1110 0000 0000
        ^
        0000 0000 0000 0000 1111 0101 1010 0101
        =
        1111 0101 1010 0101 0010 1011 1010 0101
     此時進行 (length - 1) & hash(key) 操做後,
         1111 0101 1010 0101 0010 1011 1010 0101
         &
         0000 0000 0000 0000 0000 0000 0000 1111
         =
         0000 0000 0000 0000 0000 0000 0000 0101
     此時計算出來的,是hashcode結果的後幾位的值,這樣就能夠減小衝突的發生。

 

四、put、putVal

方法做用:
  Step1: 給 HashMap 的數組 初始化。
  Step2: 定義 鏈表 轉爲 紅黑樹的條件。
  Step3: 定義數據存儲的動做(存儲的方式:鏈表仍是紅黑樹)。

(1)分析 put 過程
  Step1:put 內部調用 putVal() 方法。
  Step2:先判斷 數組是否爲 null 或者 長度爲0,是的話,則調用 resize 方法給數組擴容。
  Step3:對 key 進行 hash 並執行位運算((length - 1) & hash(key)),獲得數組下標。若不衝突,即當前數組位置不存在元素,直接在此處添加一個節點便可。
  Step4:若衝突,即當前數組位置存在元素,則根據節點的狀況進行判斷。
    若是 剛好是第一個 元素,則進行替換 value 的操做。
    若是不是第一個元素,則判斷是否爲 紅黑樹結構,是則添加一個樹節點。
    若是不是紅黑樹結構(即鏈表),則採用尾插法給鏈表插入一個節點,鏈表長度大於等於 8 時,將鏈表轉爲紅黑樹結構。
  Step5:若 Node 長度大於閾值,還得從新 resize 擴容。

(2)源碼:

// Node 數組
transient Node<K,V>[] table;

// 插入數據的操做
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

/**
* 真正的插入數據的方法。
* @param key 的 hash 值
* @param key
* @param value
* @param onlyIfAbsent爲 true,插入數據若存在值時,不會進行修改操做
* @param evict if false, the table is in creation mode.
* @return 上一個值,若不存在,則返回 null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    // 若是 Node 數組爲 null 或者 長度爲 0 時,即 Node 數組不存在,則調用 resize() 方法,從新獲取一個調整大小後的 Node 數組。
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
        
    // 若是當前數組元素沒有值,即不存在 哈希衝突的狀況,直接添加一個 Node 進去(多線程時,此處可能致使線程不安全)。
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // 存在哈希衝突的狀況下,須要找到 插入或修改 的節點的位置,而後再操做(插入或修改)
        Node<K,V> e; K k;
        
        // Step1:找到節點的位置1
        // 判斷第一個節點 是否是咱們須要找的,判斷條件: hash 是否相等、 key 是否相等。都相等則保存該節點,後續會修改。
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            // 判斷是否爲 紅黑樹節點,是則添加一個樹節點,返回節點
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 是鏈表節點,遍歷查找節點
            for (int binCount = 0; ; ++binCount) {
                // 第一個條件判斷得知 第一個節點不是咱們要的,因此能夠直接從第二個節點開始(p.next),而後遍歷得第3、四個節點。
                if ((e = p.next) == null) {
                    // 若是第二(3、四。。。)個節點沒有值,直接添加一個 Node 便可,此時的 e 爲 null。
                    p.next = newNode(hash, key, value, null);
                    
                    // 若是鏈表長度大於等於 8,則轉爲紅黑樹 ,並結束遍歷操做
                    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;
            }
        }
        
        // 當 e 不爲 null 時,對值進行修改,並將舊值返回
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 此處是 空實現,LinkedHashMap使用
            afterNodeAccess(e);
            return oldValue;
        }
    }
    
    // 添加節點的後續操做
    // 修改次數加1
    ++modCount;
    
    // 當Node節點數 size 大於 閾值時,須要執行 resize 方法調整數組長度。
    if (++size > threshold)
        resize();
    // 此處是 空實現,LinkedHashMap使用
    afterNodeInsertion(evict);
    
    // 添加節點成功,返回 null
    return null;
}

 

五、resize

  用於給數組擴容。
(1)resize 過程
  Step1:計算新數組的閾值、新數組的長度。
  Step2:給新數組複製。對於鏈表節點採用 e.hash & oldCap 去肯定元素的位置,新位置只有兩種可能(在原位置、或者在原位置的基礎上增長 舊數組長度)

【舉例:】
    e.hash = 10 = 0000 1010,     oldCap = 16 = 0001 0000
則  e.hash & oldCap = 0000 0000 = 0

    e.hash = 18 = 0001 0010,     oldCap = 16 = 0001 0000
則  e.hash & oldCap = 0001 0000 = 16

當 e.hash & oldCap == 0 時,新位置爲 原數據所在的位置。即 table[j]
當 e.hash & oldCap != 0 時,新位置爲 原數據所在的位置 + 原數組的長度。即 table[j + oldCap]

(2)源碼:

/**
* 給數組擴容
*/
final Node<K,V>[] resize() {
    // Step1:判斷數組是否須要擴容,若須要則擴容
    // 記錄原數組
    Node<K,V>[] oldTab = table;
    
    // 記錄原數組長度,若爲 null,則爲 0, 不然爲 數組的長度
    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;
        }
        // 若數組長度2倍擴容仍小於最大容量,則閾值加倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 原數組爲null,若舊閾值大於0, 則數組長度爲 閾值大小
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 原數組爲 null,舊閾值小於等於0, 則數組長度、閾值均爲默認值
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 若新閾值爲 0,則根據新數組長度從新計算閾值
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    
    // Step2:將原數組的數據複製到新數組中(從新計算元素新的位置)
    @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;
                        // 判斷節點是否須要移動,位運算 爲 0 則不移動
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 位運算不爲 0,需移動
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 將鏈表的尾節點置 null,並將頭節點放到新位置
                    if (loTail != null) {
                        loTail.next = null;
                        // 新位置爲 原始位置
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        // 新位置爲 原始位置 + 原始數組長度
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

 

六、get、getNode

  用於獲取節點的 value 值。
(1)分析 get 的過程
  Step1:先獲取節點,內部調用 getNode() 方法。
  Step2:判斷 數組是否爲 null 或者 長度爲0,是則直接返回 null。對 key 進行 hash 並執行位運算((length - 1) & hash(key)),獲得數組下標,若當前數組下標位置數據爲null,也返回 null。
  Step3:若當前數組下標位置有值。
    若 剛好是第一個元素,直接返回第一個節點便可。
    若不是第一個元素,則判斷是否爲 紅黑樹結構,是則返回樹節點。
    若不是樹結構,則遍歷鏈表,返回相應的節點。

(2)源碼:

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

/**
* 真正獲取 value 的操做
*/
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 數組長度爲 0 或者爲 null,或者 節點不存在,直接返回一個 null
    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;
}

 

3、常見面試題

一、爲何使用 數組 + 鏈表 + 紅黑樹的 數據結構?

  數組用於 肯定 數據存儲的位置,鏈表用來解決 哈希衝突(當衝突時,在當前數組對應的位置造成一個鏈表)。當鏈表的長度大於等於 8 時,須要將其轉爲 紅黑樹,查詢效率比鏈表高。
  採用數組 + 鏈表的數據結構,能夠結合 數組尋址的優點 以及 鏈表在增刪上的高效。

二、HashMap 什麼狀況下會擴容?

  當數組長度超過閾值時(loadFactor * capacity),默認負載因子(loadFactor) 爲 0.75,數組(capacity)長度 爲 16。
  此時的閾值爲 16 * 0.75 = 12,即只要數組長度大於 12 時,就會發生擴容(resize)。數組、閾值擴大到原來的 2 倍。即 當前數組長度爲 16,擴容後變爲 32,閾值爲 24。

三、數組擴容爲何長度是 2 的次冪?

  爲了實現高效、必須減小碰撞,即須要將數據儘可能均勻分配,使每一個鏈表長度大體相同。數據 key 的哈希值直接使用確定是不行的,能夠採用 取模運算 ,即 hash(key) % length,獲得的餘數做爲數組的下標( table[hash(key) % length] )。
  可是取模運算的效率沒有 移位運算高((length - 1) & hash(key))。length 指的是數組的長度。

// Node 數組
transient Node<K,V>[] table;

JDK 1.8 源碼給的實現是 
    (length - 1) & hash(key), // 計算數組下標值
    table[(length - 1) & hash(key)] // 定位到數組元素的位置
也即 
    (length - 1) & hash(key) == hash(key) % length,

想要上面等式成立, length 必須知足 2 的次冪(效率最高), 即 length = 2^n。

爲何必須知足 2 的次冪?
    由於只有 2 的次冪, length - 1 的二進制位全爲1,使得 hash(key) 後幾位都進行 &1 操做, 這樣獲得的結果等同於 hash(key) 後幾位的值。
    即 (length - 1) & hash(key) == hash(key) % length
    若是 不爲 2 的次冪,那麼可能存在 某些值永遠都不會出現的狀況。

舉個例子:
【hash(key) = 9, length = 16】
    此時 hash(key) % length = 9 % 16 = 9
    (length - 1) & hash(key) = 15 & 9 = 1111 & 1001 = 1001 = 9
    hash(key) % length == (length - 1) & hash(key)
    
【hash(key) = 27, length = 16】
    此時 hash(key) % length = 27 % 16 = 11
    (length - 1) & hash(key) = 15 & 27 = 01111 & 11011 = 1011 = 11
    hash(key) % length == (length - 1) & hash(key)
    
【hash(key) = 9, length = 15】
    此時 hash(key) % length = 9 % 15 = 9
    (length - 1) & hash(key) = 14 & 9 = 1110 & 1001 = 1000 = 8
    hash(key) % length !== (length - 1) & hash(key)
數組長度爲 15 時,length -1 = 1110,此時無論如何,最後一位均不可能爲 1,也即 100一、1101等這些值永遠都獲取不到。

 

四、String 中的 hashCode 方法

  參考:https://segmentfault.com/a/1190000010799123。
  以 31 爲權,對每個字符的 ASCII 碼進行運算。
  選用 31 的緣由,31 * i = 32 * i - i = (i << 5) - i,31 能夠被虛擬機優化成 位運算,效率更高。

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

 

五、HashMap 線程不安全?舉個例子?

  HashMap 採用尾插法將數據插入鏈表的尾部,但其 putVal 方法是線程不安全的。putVal 方法中有段代碼以下:

if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);

  當線程 A 與線程 B 同時進行 put 操做時,且兩個值的 key 通過 hash() 是一致的,即佔用同一個數組元素。若此時數組元素爲 null,線程 A 執行到這段代碼的時候,發現該位置數據爲 null,則觸發一次 newNode 操做,這時線程 B 剛好也執行到這,一樣觸發一次 newNode 操做,這時不論是線程 A 仍是線程 B成功,都會覆蓋當前元素,即線程不安全。
JDK 7 用的頭插法,會形成死循環(沒有過多研究,有時間再補充)。

六、HashMap、HashTable、ConcurrentHashMap的區別

(1)HashMap 是線程非安全的,容許存在 null 的 key 以及 null 的 value。且只有一個爲 null 的key,能夠存在多個爲 null 的 value。HashMap 的效率比 HashTable 高(2)HashTable 是線程安全的,不容許存在 null 值。(3)ConcurrentHashMap 是線程安全的 HashMap,併發能力比 HashTable 強。

相關文章
相關標籤/搜索