HashMap详解

136 阅读4分钟

Java HashMap采用数组+链表/红黑树结构,通过哈希计算和动态扩容实现高效键值存储,非线程安全,建议高并发场景使用ConcurrentHashMap。

一、核心特性

  • 键值对存储:基于 key-value 结构,允许 null 键和 null 值。
  • 无序性:不保证元素顺序(LinkedHashMap 维护插入顺序)。
  • 哈希冲突解决:链表 + 红黑树(JDK8+)。
  • 非线程安全:多线程操作需同步或使用 ConcurrentHashMap

二、底层数据结构(JDK8+)

1. 数组 + 链表 + 红黑树
  • 数组(哈希桶)Node<K,V>[] table,每个桶存放链表或红黑树。

  • 链表节点

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next; // 链表指针
    }
    
  • 红黑树节点(链表长度 ≥8 时树化):

    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // 父节点
        TreeNode<K,V> left;    // 左子树
        TreeNode<K,V> right;   // 右子树
        boolean red;           // 颜色标记
    }
    
2. 数据结构图示(Mermaid)
HashMap
桶1: 链表
数组
桶2: 红黑树
桶N: ...

image.png


三、关键实现原理

1. 哈希计算
  • 哈希扰动函数(减少碰撞):

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

    // 桶下标 = (数组长度 - 1) & hash
    int index = (n - 1) & hash;
    
2. put() 流程
UserHashMapput(key, value)计算 key 的 hash 值根据 hash 定位桶下标直接插入新 Node遍历链表/红黑树替换旧 value插入新 Node树化链表alt[链表长度≥8且数组长度≥64]alt[key已存在][key不存在]alt[桶为空][桶非空]检查是否需要扩容返回旧 value(或 null)UserHashMap

image.png

3. 扩容机制
  • 触发条件:元素数量 > 容量 × 负载因子(默认 0.75)。

  • 扩容规则

    • 新容量 = 旧容量 × 2(保证始终为 2 的幂)。

    • 重新计算所有元素的桶下标(利用高位掩码优化):

      // 例如旧容量为 16(二进制 10000),扩容后为 32(二进制 100000)
      if ((e.hash & oldCap) == 0) {
          // 新索引 = 原位置
      } else {
          // 新索引 = 原位置 + oldCap
      }
      

四、线程安全问题

  • 问题场景

    • 死循环(JDK7):多线程扩容时链表成环。
    • 数据丢失:并发插入导致覆盖。
  • 解决方案

    • 使用 ConcurrentHashMap
    • 通过 Collections.synchronizedMap() 包装(性能较低)。

五、性能优化技巧

  1. 初始化容量

    // 预估元素数量,避免频繁扩容
    new HashMap<>(initialCapacity);
    
  2. 哈希函数设计

    • 键对象必须正确重写 hashCode()equals()
    • 避免哈希冲突(如 StringInteger 等不可变对象作为键)。
  3. 负载因子调整

    // 降低负载因子(如 0.5)以减少冲突,但增加内存占用
    new HashMap<>(16, 0.5f);
    

六、与其他 Map 实现对比

特性HashMapLinkedHashMapTreeMapConcurrentHashMap
顺序无序插入顺序/访问顺序排序无序
底层结构数组+链表/红黑树哈希表 + 双向链表红黑树分段数组 + CAS
线程安全
时间复杂度(平均)O(1)O(1)O(log n)O(1)
适用场景通用键值存储需要顺序或 LRU 缓存需要排序或范围查询高并发场景

七、源码分析(关键方法)

1. putVal() 核心逻辑(简化版)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent) {
    Node<K,V>[] tab; Node<K,V> 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<K,V> e; K k;
        // 3. 检查头节点是否匹配
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 红黑树插入
        else {
            // 4. 遍历链表
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash); // 树化
                    break;
                }
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 5. 替换旧值
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            return oldValue;
        }
    }
    // 6. 检查扩容
    if (++size > threshold)
        resize();
    return null;
}
2. resize() 扩容逻辑
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 计算新容量和阈值...
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 迁移数据
    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 {
                // 链表拆分(lo/hi 链)
                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);
                if (loTail != null) {
                    loTail.next = null;
                    newTab[j] = loHead; // 原索引位置
                }
                if (hiTail != null) {
                    hiTail.next = null;
                    newTab[j + oldCap] = hiHead; // 新索引位置
                }
            }
        }
    }
    return newTab;
}

八、总结

  • 核心优势:高效的随机访问(平均 O(1) 时间复杂度)。

  • 使用注意

    • 键对象必须正确实现 hashCode()equals()
    • 避免在多线程环境中直接使用。
  • 最佳实践

    • 预估初始容量,减少扩容开销。
    • 高并发场景选择 ConcurrentHashMap
    • 需要顺序访问时使用 LinkedHashMap