Java集合之HashMap

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

1、Map類特色:node

  一、Key不能重複,Value可重複算法

  二、Value能夠是List、Map、Set類對象數組

  三、KV是否容許爲null,以實現類約束爲準安全

2、Map除提供增刪改查外,還有三個Map特有方法。多線程

  一、返回全部的Keyapp

Set<K> keySet();

  返回Map類杜希昂中的Key的Set視圖。函數

  二、返回全部value性能

Collection<V> values();

  返回Map類對象中的全部Value的Collection視圖。測試

  三、返回全部K-V鍵值對

Set<Map.Entry<K, V>> entrySet();

  返回Map類對象中的K-V鍵值對的Set視圖。

   這些函數返回的視圖支持清除操做(remove、clear),不支持修改和添加元素。

3、主要的Map類集合(圖來自Java開發手冊pdf)

Hashtable逐漸棄用,ConcurrentHashMap多線程比HashMap安全,但本文主要分析HashMap。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

HashMap

1、哈希類集合的三個基本存儲概念

名稱 說明
table 存儲全部節點數據的數組
slot 哈希槽。即table[i]這個位置
bucket 哈希桶。table[i]上全部元素造成的表或數的集合

 

 

 

 

圖示:

鏈表Node「Node是HashMap的一個靜態內部類。

//Node是單向鏈表,實現了Map.Entry接口
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;
    }

    // getter and setter ... toString ...
    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;
    }
}

紅黑樹TreeNode:經過特定着色的旋轉(左旋、右旋)來保證從根節點到葉子節點的最長路徑不超過最短路徑的2倍的二叉樹,相比AVL樹,更加高效的完成插入和刪除操做後的自平衡調整。最壞運行時間爲O(logN).

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    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);
    }

    /**
     * Returns root of tree containing this node.
     */
    final TreeNode<K,V> root() {
        for (TreeNode<K,V> r = this, p;;) {
            if ((p = r.parent) == null)
                return r;
            r = p;
        }
    }

2、HashMap定義變量

/**
 * 默認初始容量16(必須是2的冪次方)
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

/**
 * 最大容量,2的30次方
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 默認加載因子,用來計算threshold
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 鏈表轉成樹的閾值,當桶中鏈表長度大於8時轉成樹 
   threshold = capacity * loadFactor
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 進行resize操做時,若桶中數量少於6則從樹轉成鏈表
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 桶中結構轉化爲紅黑樹對應的table的最小大小

 當須要將解決 hash 衝突的鏈表轉變爲紅黑樹時,
 須要判斷下此時數組容量,
 如果因爲數組容量過小(小於 MIN_TREEIFY_CAPACITY )
 致使的 hash 衝突太多,則不進行鏈表轉變爲紅黑樹操做,
 轉爲利用 resize() 函數對 hashMap 擴容
 */
static final int MIN_TREEIFY_CAPACITY = 64;
/**
 保存Node<K,V>節點的數組
 該表在首次使用時初始化,並根據須要調整大小。 分配時,
 長度始終是2的冪。
 */
transient Node<K,V>[] table;

/**
 * 存放具體元素的集
 */
transient Set<Map.Entry<K,V>> entrySet;

/**
 * 記錄 hashMap 當前存儲的元素的數量
 */
transient int size;

/**
 * 每次更改map結構的計數器
 */
transient int modCount;

/**
 * 臨界值 當實際大小(容量*負載因子)超過臨界值時,會進行擴容
 */
int threshold;

/**
 * 負載因子
 */
final float loadFactor;

默認容量:16   DEFAULT_INITIAL_CAPACITY = 1 << 4

自定義初始化容量:構造函數 ↓

Map容量必定爲2的冪次。

默認加載因子:0.75  DEFAULT_LOAD_FACTOR = 0.75f

自定義負載因子:構造函數 ↓

桶中節點從鏈表轉化爲紅黑樹:節點數大於8

桶中元素從紅黑樹返回爲鏈表:節點數小於等於6

threshold:臨界值 = 容量×負載因子,當實際容量大於臨界值,爲了減少哈希衝突,進行擴容。

3、構造函數

/**
 * 傳入初始容量大小,使用默認負載因子值 來初始化HashMap對象
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * 默認容量和負載因子
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
 * 傳入初始容量大小和負載因子 來初始化HashMap對象
 */
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;
    // 初始化threshold大小
    this.threshold = tableSizeFor(initialCapacity);
}

/**
 * 找到大於或等於 cap 的最小2的整數次冪的數。
 */
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;
}

tableSizeFor(int cap):用位運算找到大於或等於 cap 的最小2的整數次冪的數。好比10,則返回16。

4、put函數

public V put(K key, V value) {
    // 調用hash(key)方法來計算hash 
    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;
    // 容量初始化:當table爲空,則調用resize()方法來初始化容器
    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 {
        Node<K,V> e; K k;
        // 比較桶中第一個元素(數組中的結點)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //若是鍵的值以及節點 hash 等於鏈表中的第一個鍵值對節點時,則將 e 指向該鍵值對
            e = p;
        // 若是桶中的引用類型爲 TreeNode,則調用紅黑樹的插入方法
        else if (p instanceof TreeNode)
            // 放入樹中
            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;
                }
                // 判斷鏈表中結點的key值與插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //判斷要插入的鍵值對是否存在 HashMap 中
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // onlyIfAbsent 表示是否僅在 oldValue 爲 null 的狀況下更新鍵值對的值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 鍵值對數量超過閾值時,則進行擴容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

在new HashMap() 完成後,若沒有put操做,是不會分配存儲空間的。

  1. 當桶數組 table 爲空時,經過擴容的方式初始化 table

  2. 查找要插入的鍵值對是否已經存在,存在的話根據條件判斷是否用新值替換舊值

  3. 若是不存在,則將鍵值對鏈入鏈表中,並根據鏈表長度決定是否將鏈表轉爲紅黑樹

  4. 判斷鍵值對數量是否大於閾值,大於的話則進行擴容操做

5、hash值的計算

  • 根據存入的key-value對中的key計算出對應的hash值,而後放入對應的桶中,因此好的hash值計算方法十分重要,能夠大大避免哈希衝突。
  • HashMap是以hash操做做爲散列依據。可是又與傳統的hash存在着少量的優化。其hash值是key的hashcode與其hashcode右移16位的異或結果。在put方法中,將取出的hash值與當前的hashmap容量-1進行與運算。獲得的就是位桶的下標。那麼爲什麼須要使用key.hashCode() ^ h>>>16的方式來計算hash值呢。其實從微觀的角度來看,這種方法與直接去key的哈希值返回在功能實現上沒有差異。可是因爲最終獲取下表是對二進制數組最後幾位的與操做。因此直接取hash值會丟失高位的數據,從而增大沖突引發的可能。因爲hash值是32位的二進制數。將高位的16位於低位的16位進行異或操做,便可將高位的信息存儲到低位。所以該函數也叫作擾亂函數。目的就是減小衝突出現的可能性。而官方給出的測試報告也驗證了這一點。直接使用key的hash算法與擾亂函數的hash算法衝突機率相差10%左右。

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

    n = table.length;
    index = (n-1) & hash;
  • 根據以上可知,hashcode是一個32位的值,用高16位與低16位進行異或,緣由在於求index是是用 (n-1) & hash ,若是hashmap的capcity很小的話,那麼對於兩個高位不一樣,低位相同的hashcode,可能最終會裝入同一個桶中。那麼會形成hash衝突,好的散列函數,應該儘可能在計算hash時,把全部的位的信息都用上,這樣才能儘量避免衝突。這就是爲何用高16位與低16位進行異或的緣由。
  • 爲何capcity是2的冪?由於 算index時用的是(n-1) & hash,這樣就能保證n -1是全爲1的二進制數,若是不全爲1的話,存在某一位爲0,那麼0,1與0與的結果都是0,這樣便有可能將兩個hash不一樣的值最終裝入同一個桶中,形成衝突。因此必須是2的冪。例子:十進制16 轉化爲 二進制10000,則16-1爲 1111
  • 在算index時,用位運算(n-1) & hash而不是模運算 hash % n的好處(在HashTable中依舊是取模運算)?

    1. 位運算消耗資源更少,更有效率
    2. 避免了hashcode爲負數的狀況

6、擴容機制resize

在 HashMap 中,桶數組的長度均是2的冪,閾值大小爲桶數組長度與負載因子的乘積。當 HashMap 中的鍵值對數量超過閾值時,進行擴容。

HashMap 按當前桶數組長度的2倍進行擴容,閾值也變爲原來的2倍(若是計算過程當中,閾值溢出歸零,則按閾值公式從新計算)。擴容以後,要從新計算鍵值對的位置,並把它們移動到合適的位置上去。

final Node<K,V>[] resize() {
    // 拿到數組桶
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 若是數組桶的容量大與0
    if (oldCap > 0) {
        // 若是比最大值還大,則賦值爲最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 若是擴容後小於最大值 並且 舊數組桶大於初始容量16, 閾值左移1(擴大2倍)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 若是數組桶容量<=0 且 舊閾值 >0
    else if (oldThr > 0) // initial capacity was placed in threshold
        // 新容量=舊閾值
        newCap = oldThr;
    // 若是數組桶容量<=0 且 舊閾值 <=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;
    @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;
}

總體步驟:

  1. 計算新桶數組的容量 newCap 和新閾值 newThr

  2. 根據計算出的 newCap 建立新的桶數組,桶數組 table 也是在這裏進行初始化的

  3. 將鍵值對節點從新映射到新的桶數組裏。若是節點是 TreeNode 類型,則須要拆分成黑樹。若是是普通節點,則節點按原順序進行分組。

7、經常使用Map遍歷方法

public class Test {
    public static void main(String[] args) {
        List list = new ArrayList();
        Map<String, String> map = new HashMap<String, String>();
        map.put("1", "value1");
        map.put("2", "value2");
        map.put("3", "value3");

        //第一種:廣泛使用,二次取值
        System.out.println("經過Map.keySet遍歷key和value:");
        for (String key : map.keySet()) {
            System.out.println("key= " + key + " and value= " + map.get(key));
        }

        //第二種:推薦,尤爲是容量大時
        System.out.println("經過Map.entrySet遍歷key和value");
        for (Map.Entry<String, String> entry : map.entrySet()) {
            System.out.println(entry);
            System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
        }
    }
}

 

附:

一、JDK.7是基於數組加單鏈表實現(爲何不要雙鏈表)

  首先,用鏈表是爲了解決hash衝突。

  單鏈表能實現爲何要用雙鏈表呢?(雙鏈表須要更大的存儲空間)

二、爲何要用紅黑樹,不是平衡二叉樹?

  插入效率比平衡二叉樹高,查詢效率比普通二叉樹高。因此選擇性能相對摺中的紅黑樹。

三、重寫對象的equals時必定須要重寫hashcode,爲何?

  判斷兩個對象是否相同,首先判斷兩個對象的hashcode是否相等,若不相等,直接返回false;若相等,使用equals判斷。

  即equals判斷相等,則hashcode必定相等,hashcode相等,他們並不必定相同。

四、爲何默認加載因子爲0.75?

  調低負載因子時,HashMap 所能容納的鍵值對數量變少。擴容時,從新將鍵值對存儲新的桶數組裏,鍵的鍵之間產生的碰撞會降低,鏈表長度變短。此時,HashMap 的增刪改查等操做的效率將會變高,這裏是典型的拿空間換時間。

  相反,若是增長負載因子(負載因子能夠大於1),HashMap 所能容納的鍵值對數量變多,空間利用率高,但碰撞率也高。這意味着鏈表長度變長,效率也隨之下降,這種狀況是拿時間換空間。至於負載因子怎麼調節,這個看使用場景了。

  通常狀況下,咱們用默認值就能夠了。大多數狀況下0.75在時間跟空間代價上達到了平衡因此不建議修改。

 

 

參考資料:Java知音公衆號資源

       博客 https://blog.csdn.net/zjxxyz123/article/details/81111627

 

List參見上一篇博客:Java集合之ArrayList與LinkedList

相關文章
相關標籤/搜索