HashMap

一、结构

1.数组(桶数组)

  • 初始容量默认16。
  • 数组元素成为桶,每个桶存储链表或红黑树(jdk1.8及以后)。

2.链表

  • 当不同key的哈希值映射到同一桶式,以链表形式存储。

3.红黑树

  • jdk1.8及以后引入红黑树:当链表长度大于等于8且桶数组长度大于等于64式,链表转化为红黑树,查询时间从O(n)降为O(log n)。
  • 树节点小于6时退化为链表

二、关键机制

1.哈希计算(jdk1.8)

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // 扰动函数
}
  • 扰动函数:将 hashCode() 的高 16 位与低 16 位异或,减少哈希冲突。
  • 定位桶索引:
    index = (n - 1) & hash (n 为桶数组长度,等价于取模运算 hash % n,但位运算更快)。

2.解决哈希冲突

  • 拉链发:冲突的键值对存储在同一个桶的链表/树中。
  • 开放寻址法:Hash未使用(ThreadLocalMap使用)

3.put操作

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
  • 1.计算key和hash值,定位桶索引i
  • 2.若索引为空,直接创建新节点存入。
  • 3.桶不为空:
    • 链表:遍历链表,遇到相同 key 则更新 value;否则尾部插入。若链表长度 ≥ 8,触发树化。
    • 红黑树:调用树节点的插入方法。

4.扩容机制

1.jdk1.8之前

头插法 + 全量 rehash

扩容流程:

  1. 触发条件:
    当 size > 阈值(容量 × 负载因子) 时触发扩容。
  2. 创建新数组:
    新数组为原数组两倍
  3. 数据迁移(全量rehash):
void transfer(Entry[] newTable) {
    for (Entry<K,V> e : oldTable) {        // 遍历旧数组
        while (null != e) {
            Entry<K,V> next = e.next;      // 记录下一个节点
            int i = indexFor(e.hash, newCapacity);  // 重新计算索引
            e.next = newTable[i];          // ⚠️ 头插法:新节点指向桶头
            newTable[i] = e;               // 新桶头更新
            e = next;                      // 处理下一节点
        }
    }
}
  1. 头插可能的问题:
  • 1.节点顺序反转(原 A→B→C 变为 C→B→A)
  • 2.并发死循环风险
  1. 索引计算:
    每个节点都需要重新计算索引:
    index = (newCap-1)&hash(全量rehash)

2.jdk1.8的扩容机制

尾插法 + 高低位拆分 + 无需 rehash

扩容流程:

  1. 触发条件:
    和jdk1.7一样(size > 容量 × 负载因子)。
  2. 创建新数组:
    创建一个旧数组两倍的新数组
  3. 数据迁移:
// 遍历旧数组每个桶
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 {  // ⭐ 链表优化拆分
            Node<K,V> loHead = null, loTail = null; // 低位链表
            Node<K,V> hiHead = null, hiTail = null; // 高位链表
            do {
                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 = e.next) != null);
            
            // 低位链表放原索引 j
            if (loTail != null) {
                loTail.next = null;
                newTab[j] = loHead;
            }
            // 高位链表放新索引 j + oldCap
            if (hiTail != null) {
                hiTail.next = null;
                newTab[j + oldCap] = hiHead;
            }
        }
    }
}

与jdk1.7不同之处
1.不用rehash计算:

  • 利用位运算(e.hash & oldCap) == 0 判断位置:
    • =0→ 新索引 = 原索引 j(低位链表)
    • ≠0 → 新索引 = 原索引 j + oldCap(高位链表)

2.尾插法:

  • 保持节点原始顺序(A→B→C 迁移后仍是 A→B→C)
  • 解决并发环形链表问题(但线程安全问题仍存在)

三、常见问题

1.为什么容量总是2的幂

  • 保证 (n-1) & hash 均匀分布,等价于取模运算,且位运算更快。
  • 扩容时可直接通过高位判断新位置(newIndex = oldIndex 或 oldIndex + oldCap)。

2.HashMap是线程安全的吗

  • HashMap 非线程安全,多线程环境可能造成死循环(Java 7 链表头插法导致)或数据覆盖(Java 8)。
  • 解决:使用 ConcurrentHashMap 或 Collections.synchronizedMap()。

3.为什么树化阈值是8

基于泊松分布:哈希冲突达到 8 的概率极低(小于千万分之一),避免不必要的树化开销。

你可能感兴趣的:(哈希算法,散列表,算法)