詳解HashMap原始碼解析(上)

2022-07-04 12:02:18

jdk版本:1.8

資料結構:

HashMap的底層主要基於陣列+連結串列/紅黑樹實現,陣列優點就是查詢塊HashMap通過計算hash碼獲取到陣列的下標來查詢資料。同樣也可以通過hash碼得到陣列下標,存放資料。

雜湊表為了解決衝突,HashMap採用了連結串列法,新增的資料存放在連結串列中,如果傳送衝突,將資料放入連結串列尾部。

上圖左側部分是一個雜湊表,也稱為雜湊陣列(hash table):

// table陣列
transient Node<K,V>[] table;

table陣列的參照型別是Node節點,陣列中的每個元素都是單連結串列的頭結點,連結串列主要為了解決上面說的hash衝突,Node節點包含:

  • hash hash值
  • key
  • value
  • next next指標

Node節點結構如下:

 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;
    }
    // 省略 get/set等方法
}

主要屬性

// 儲存元素陣列
Node<K,V>[] table;

// 元素個數
int size;

// 陣列擴容臨界值,計算為:元素容量*裝載因子
int threshold

// 裝載因子,預設0.75
float loadFactor;

// 連結串列長度為 8 的時候會轉為紅黑樹
int TREEIFY_THRESHOLD = 8;

// 長度為 6 的時候會從紅黑樹轉為連結串列
int UNTREEIFY_THRESHOLD = 6;

  • size記錄元素個數
  • threshold 擴容的臨界值,等於元素容量*裝載因子
  • TREEIFY_THRESHOLD 8 連結串列個數增加到8會轉成紅黑樹
  • UNTREEIFY_THRESHOLD 6 連結串列個數減少到6會退化成連結串列
  • loadFactor 裝載因子,預設為0.75

loadFactor 裝載因子等於擴容閾值/陣列長度,表示元素被填滿的程式,越高表示空間利用率越高,但是hash衝突的概率增加,連結串列越長,查詢的效率降低。越低hash衝突減少了,資料查詢效率更高。但是示空間利用率越低,很多空間沒用又繼續擴容。為了均衡查詢時間使用空間,系統預設裝載因子為0.75

獲取雜湊陣列下標

新增、刪除和查詢方法,都需要先獲取雜湊陣列的下標位置,首先通過hash演演算法算出hash值,然後再進行長度取模,就可以獲取到元素的陣列下標了。

首先是呼叫hash方法,計算出hash值

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

先獲取hashCode值,然後進行高位運算,高位運算後的資料,再進行取模運算的速度更快。

算出hash值之後,再進行取模運算:

(n - 1) & hash

上面的n是長度,計算的結果就是陣列的下標了。

構造方法

HashMap()

     /**
     * default initial capacity (16)
     *  the default load factor (0.75). 
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
    }

設定預設裝載因子0.75,預設容量16

HashMap(int initialCapacity)

// 指定初始值大小
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

// 指定初始值和預設裝載因子 0.75
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);
}

HashMap(int initialCapacity) 指定初始容量,呼叫HashMap(int initialCapacity, float loadFactor) 其中loadFactor為預設的0.75

首先做容量的校驗,小於零報錯,大於最大容量賦值最大值容量。然後做裝載因子的校驗,小於零或者是非數位就報錯。

tableSizeFor使用右移和或運算,保證容量是2的冪次方,傳入2的冪次方,返回傳入的資料。傳入不是2的冪次方資料,返回大於傳入資料並接近2的冪次方數。比如:

  • 傳入10返回16
  • 傳入21返回32

HashMap(Map<? extends K, ? extends V> m)

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

將集合m的資料新增到HashMap集合中,先設定預設裝載因子,然後呼叫putMapEntries新增集合元素到HashMap中,putMapEntries是遍歷陣列,新增資料。

總結

本文基於jdk1.8解析HashMap原始碼,主要介紹了:

  • HashMap 是基於陣列+連結串列/紅黑樹結構實現。採用連結串列法解決hash衝突。
  • Node 節點記錄了資料的keyhashvalue以及next指標。
  • HashMap主要屬性:
    • size 元素個數
    • table[] 雜湊陣列
    • threshold 擴容的閾值
    • loadFactor 裝載因子
    • TREEIFY_THRESHOLD 8,連結串列個數為8轉成紅黑樹。
    • UNTREEIFY_THRESHOLD 6 ,連結串列個數為6紅黑樹轉為連結串列。
  • 新增、刪除以及查詢元素,首先要先獲取陣列下標,HashMap先呼叫hasCode方法,hashCode()的高16位元互斥或低16位元,大大的增加了運算速度。然後再對陣列長度進行取模運算。本質就是取key的hashCode值、高位運算、取模運算
  • HashMap幾個構造方法:
    • HashMap()設定預設裝載因子0.75和預設容量16
    • HashMap(int initialCapacity)設定初始容量,預設裝載因子0.75,容量是一定要是2的冪次方,如果不是2的冪次方,增加到接近2的冪次方數。
    • HashMap(Map<? extends K, ? extends V> m)主要是遍歷新增的集合,新增資料。

參考

深入淺出HashMap的設計與優化

Java 8系列之重新認識HashMap

感覺不錯的話,就點個贊吧!