JDK容器學習之HashMap (一) : 底層存儲結構分析

底層數據結構

首先經過源碼,類中的field以下,java

transient Node<K,V>[] table;

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

transient int size;

transient int modCount;

int threshold;

final float loadFactor;

其中 Node, Map.Entry 是兩個比較核心的數據結構,先看下Node的定義數組

1. Map.Entry

Map接口中內部定義的接口, 提供了操做Map中鍵值對的基本方法數據結構

一個Entry對象,表明了Map中的一個鍵值對,能夠經過它獲取key,value也能夠從新設置valueapp

interface Entry<K,V> {
  K getKey();

  V getValue();

  V setValue(V value);

  boolean equals(Object o);

  int hashCode();
}

依次說明下上面的每一個方法的做用工具

獲取鍵 : K getKey()

獲取值 : V getValue()

設置值 : V setValue(V value)

haseCode 方法

返回entry 的 hash code, 定義以下:學習

(e.getKey()==null   ? 0 : e.getKey().hashCode()) ^
(e.getValue()==null ? 0 : e.getValue().hashCode())

確保兩個 Entry對象 equals返回true,則hashcode的值必然相同this

equals 方法

當兩個entry對象表示的是同一個映射關係時,返回true.net

規則以下設計

(e1.getKey()==null ? e2.getKey()==null : e1.getKey().equals(e2.getKey()))   &&
(e1.getValue()==null ? e2.getValue() ==null : e1.getValue().equals(e2.getValue()))

2. Node<K, V>

做爲HashMap中對 Map.Entry的實現,具體邏輯以下code

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

說明

  • hash 這個字段是幹嗎的
  • 爲何要有一個next元素

3. Node<K,V>[] table; 說明

按咱們的理解,map是一個kv結構,每一個Node對象表示的就是一個kv對,那麼這個table應該就是保存全部的kv對的數據結構了

爲何會是一個數組? 怎麼根據key來定位kv對在數組中的位置?

a. 前置說明

table數組大小,必須爲2的n次方,首次使用是初始化,必要時(如添加新的kv對時)能夠擴充容量

要了解這個數組的使用過程,最佳的思路就是經過三個方法來定位了

  1. new HashMap<>() 建立對象時,數組的初始化
  2. put(k,v) 添加kv時,數組的擴容以及塞值
  3. get(k) 經過key獲取value時,在數組中的定位

b. 建立對象

構造方法以下,主要是設置了閥值,loadFactory (後面說其用處)

public HashMap(int initialCapacity, float loadFactor) {
    // 參數合法性校驗省略
    
    this.loadFactor = loadFactor;
    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;
}

上面的構造,並無如咱們預期的初始化 table 數組,接下來看put方法,是否有設置 table數組呢


c. 添加kv : put(k,v)

實現以下,邏輯比較複雜,會直接在代碼中給出一些註釋

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

/**
 * Implements Map.put and related methods
 *
 * @param hash (key的hash值,經過hash方法計算)
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent true表示在不存在kv時,才塞入數據
 * @param evict if false, the table is in creation mode.
 * @return 返回原來的value(若是以前不存在,返回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;
    
    // 首先是將tab局部變量指向 table數組
    if ((tab = table) == null || (n = tab.length) == 0) {
        // 當table數組沒有初始化時,進行初始化,並返回數組長度
        n = (tab = resize()).length;
    } 
    
    
    if ((p = tab[i = (n - 1) & hash]) == null) {
        // 根據數組長度和key的hash值,計算出key放入數組的位置,若該位置沒有值,則直接建立一個新的Entry(即Node),放在該位置便可
        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)))) {
        // 若根據key的hash值,從數組中獲取的Entry對象,其key正好是咱們指定的key,則直接修改這個Entry的value值便可
            e = p;
        }
        // 下面則表示出現hash碰撞,雖然key的hash值相同,可是這個Entry的key並非咱們指定的key
        else if (p instanceof TreeNode) {
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        } else {
        // 迭代Entry的next節點,知道找到Entry.Key 正好是咱們指定的Key爲止
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) { // 若一直都不存在,則建立一個新的Entry對象,並塞入table
                    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)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

邏輯拆解:

  • 判斷 table數組是否初始化,不然進行初始化
  • 計算key的hash值(經過hash()方法獲取)
  • 以key的hash值計算索引,到table數組中查詢Node節點
    • 若不存在,則新建一個Node節點,塞入該位置
    • 若存在,則繼續判斷該節點的key是否和傳入的key相同or相等(equals()方法)
      • 是,則直接修改這個Node節點的value值便可
      • 否,表示出現hash碰撞了,須要遍歷Node節點內部的next節點,直到到next節點爲null(新建一個Node節點)或next節點就是咱們但願的節點(更新該節點value值)爲止

到這裏就能夠解決在介紹Node類結構的兩個問題

  1. Node中的hash字段幹嗎的?
  • hash字段保存的是Key經過hash()方法計算的值
  • 能夠用於判斷一個Node是否爲咱們查找的節點
  1. Node中爲何有next節點
  • next節點存的是相同 hash值的kv鍵值對,由此能夠看出HashMap的存儲結構
  • 當出現hash碰撞時,即對於計算key的hash值相同的Node節點,以鏈表結構存在

輸入圖片說明


d. table數組初始化

push(k,v) 包含較多的內容,上面只給出了設計邏輯,具體實現有必要扣一扣,研究下其中一些有意思的點

從上面的的代碼能夠看出,調用 resize() 方法進行的初始化(此外這個方法也負責數組的擴容)

源碼實現比較長,這裏主要關注初始化過程,以如下面這段邏輯進行實例分析

Map map = new HashMap<>();
map.put(xx, xx);

對resize方法中一些邏輯配合上面的使用方式進行簡化處理, 抽出代碼以下

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table; // null
    int oldCap = 0;
    int oldThr = 0;
    int newCap, newThr = 0;

    // zero initial threshold signifies using defaults
    newCap = 16; // DEFAULT_INITIAL_CAPACITY;
    newThr = 12; // (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

 
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    return newTab;
}

上面是簡化resize的內部邏輯,單獨剝離出初始化 table 數組的代碼塊;

說明

  • 初始化的數組長度爲16
  • threshold 閥值爲12 : 0.75 * 數組長度

e. hash方法

計算key的hash值,這個直接決定hash碰撞的機率

實現以下

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

到這裏天然就會有一個疑問

如何根據hash值與table數組進行關聯,又如何保證碰撞較小?

這個問題單獨成篇,再將這個,這裏先記下


小結

1. 存儲結構

HashMap 的底層數據結構是一個Node數組,配合Node鏈表的方式進行kv存儲

2. 初始化

數組的初始化延遲在首次向Map中添加元素時進行

默認數組長度爲16,閥值爲12

閥值定義爲: The next size value at which to resize (capacity * load factor).

3. 數組長度要求

數組長度要求爲2的n次方

tableSizeFor 方法實現獲取正大於數字n的2的整數次冪 (這個實現比較有意思)

4. 獲取Entry對象

如何經過key獲取對應的Entry對象呢?

  • hash()方法計算key的hash值
  • hash值定位 table數組中的下標
  • 取出數組中的 Node 節點
    • null,表示不存在
    • 非null,判斷Node節點的key是否等同輸入key
      • 是直接返回
      • 不然遍歷 Nodenext節點,直到爲null或者找到爲止

我的信息

關注小灰灰blog,一塊兒學習

輸入圖片說明

參考

相關文章
相關標籤/搜索