首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

HashMap 核心原理深度解析:哈希算法、Put/Get 流程、擴容機制、紅黑樹轉換與 JDK 8 優化

     編輯:Java 綜合教學

前言:HashMap 是 Java 面試與實戰的「必考王」

基於數組 + 鏈表 + 紅黑樹 實現 Map 接口,JDK 8 引入尾插法、紅黑樹化、擴容優化 大幅提升性能。本文從接口定義、四大構造器、哈希碼計算、put/get 核心流程、擴容遷移、樹化閾值、線程不安全根因全方位剖析。

一、Map 接口與 HashMap 定位

接口/類特點
Map鍵值對集合,鍵唯一,值可重複
Map.Entry鍵值對實體,內部類
SortedMap鍵有序
HashMap哈希表,無序,允許 null 鍵/值,非線程安全
TreeMap紅黑樹,有序
Hashtable同步,舊 API,不允許 null
ConcurrentHashMap高併發,分段鎖/CAS

二、四大構造器與核心參數

// 1. 默認:容量 16,負載因子 0.75
new HashMap<>();

// 2. 指定初始容量 (避免擴容)
new HashMap<>(int initialCapacity);

// 3. 指定容量+負載因子
new HashMap<>(int initialCapacity, float loadFactor);

// 4. 複製現有 Map
new HashMap<>(Map m);

核心參數

  • DEFAULT_INITIAL_CAPACITY = 16 (必須是 2 的冪)。
  • MAXIMUM_CAPACITY = 1 << 30。
  • DEFAULT_LOAD_FACTOR = 0.75f (空間/時間平衡點)。
  • TREEIFY_THRESHOLD = 8:鏈表長度 ≥ 8 且容量 ≥ 64 → 紅黑樹。
  • UNTREEIFY_THRESHOLD = 6:縮容/遷移時節點 ≤ 6 → 退化鏈表。
  • MIN_TREEIFY_CAPACITY = 64:容量未達 64 時優先擴容而非樹化。

三、哈希碼計算:擾動函數降低碰撞

// JDK 8 靜態擾動函數
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// 索引計算 (位運算替代取模,前提 capacity 是 2 的冪)
int index = (n - 1) & hash;

原理:高 16 位參與低位運算,分散高位變化導致的碰撞。null 鍵固定放桶 0。

四、Put 流程圖解 (JDK 8)

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[] tab; Node p; int n, i;
    // 1. 首次擴容/初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

    // 2. 計算桶索引,桶為空直接放入
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node e; K k;
        // 3. 鍵已存在 (hash 相等 + key equals) → 覆蓋值
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4. 紅黑樹節點 → 樹插入
        else if (p instanceof TreeNode)
            e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
        // 5. 鏈表節點 → 遍歷尾部尾插法插入
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 6. 達到樹化閾值 → 轉紅黑樹
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    e = e; break; // 找到覆蓋點
                }
                p = e;
            }
        }
        if (e != null) { // 覆蓋舊值
            V oldValue = e.value;
            if (!onlyIfAbsent) e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold) resize(); // 擴容檢查
    afterNodeInsertion(evict);
    return null;
}

關鍵優化點

  1. 尾插法:JDK 7 頭插法導致並發擴容環形鏈表,JDK 8 尾插保持原序,消除死循環風險。
  2. 樹化:長鏈表 O(n) → 紅黑樹 O(log n),防止惡意碰撞攻擊 (HashDoS)。
  3. 擴容優化:e.hash & oldCap 判斷高位變化,原地遷移或偏移 oldCap,無需重新計算 hash。

五、Get 流程:極簡高效

public V get(Object key) {
    Node e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node getNode(int hash, Object key) {
    Node[] tab; Node first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 桶首節點直接命中
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 紅黑樹查找
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode)first).getTreeNode(hash, key);
            // 鏈表遍歷
            do {
                if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

六、擴容機制:resize() 核心邏輯

final Node[] resize() {
    Node[] 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; }
        newCap = oldCap << 1; // 翻倍
        newThr = oldThr << 1;
    } else if (oldThr > 0) {
        newCap = oldThr;
    } else {
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node[] newTab = (Node[])new Node[newCap];
    table = newTab;

    if (oldTab != null) {
        // 遷移每個桶
        for (int j = 0; j < oldCap; ++j) {
            Node e = oldTab[j];
            if (e != null) {
                oldTab[j] = null; // 釋放引用
                if (e.next == null) // 單節點直接遷移
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode) // 紅黑樹拆分遷移
                    ((TreeNode)e).split(this, newTab, j, oldCap);
                else { // 鏈表拆分遷移 (JDK 8 核心優化)
                    Node loHead = null, loTail = null;
                    Node hiHead = null, hiTail = null;
                    Node next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) { // 高位不變 → 留原索引
                            if (loTail == null) loHead = e;
                            else loTail.next = e;
                            loTail = e;
                        } else { // 高位變化 → 索引 + oldCap
                            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;
}

拆分遷移原理

e.hash & oldCap 判斷擴容前後高位是否變化:

  • == 0:高位不變 → 新索引 = 舊索引 j (低位組合不變)。
  • != 0:高位變化 → 新索引 = j + oldCap (高位多出 1,索引偏移舊容量)。

單次遍歷即可完成拆分,**無需重新計算 hash**,性能大幅提升。

七、紅黑樹轉換:TreeNode 核心

  • TreeNode 繼承 Node,增加 parent/left/right/red 字段。
  • 樹化條件:binCount >= 8 且 capacity >= 64。
  • 退化條件:擴容/移除後節點 <= 6。
  • 樹操作:左旋/右旋/着色修復,保證 O(log n) 查找。

八、線程不安全三大癥狀 (回顧)

  1. 並發擴容環形鏈表 (JDK 7 頭插法) → JDK 8 尾插法修復。
  2. 數據覆蓋丟失:多線程同桶 newNode 覆蓋。
  3. size 計數錯亂:size++ 非原子。

結論:多線程環境**必須用 ConcurrentHashMap**,HashMap 絕不裸用。

九、總結

HashMap = 數組 + 鏈表/紅黑樹,JDK 8 關鍵優化:**尾插法保序、擴容拆分無需重算 hash、樹化防退化**。掌握 hash() 擾動、putVal 覆蓋/插入/樹化邏輯、resize 高位判斷拆分、getNode 雙路徑查找,即通透底層。面試必會:**擴容機制、樹化閾值、JDK 7/8 差異、線程不安全根因、ConcurrentHashMap 對比**。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved