一句话说透Java里面的HashMap的工作原理和实现

240 阅读3分钟

一句话总结
HashMap 是基于哈希表的键值对集合,通过哈希函数快速定位数据,用链表和红黑树解决冲突,支持动态扩容。


一、核心结构与初始化

  1. 底层结构

    • 数组(桶) :存储链表的头节点或红黑树的根节点。

    • 链表/红黑树:解决哈希冲突,链表长度超过8时转红黑树(退化阈值为6)。

    • 默认参数

      • 初始容量:16
      • 负载因子:0.75(扩容阈值=容量×负载因子)
      • 树化阈值:链表长度≥8
      • 链化阈值:红黑树节点≤6
  2. 初始化示例

    // 默认初始化(容量16,负载因子0.75)
    HashMap<String, Integer> map = new HashMap<>();
    
    // 自定义初始化(容量32,负载因子0.6)
    HashMap<String, Integer> customMap = new HashMap<>(32, 0.6f);
    

二、哈希函数与索引计算

  1. 哈希函数优化

    • 对键的 hashCode() 进行扰动,避免低位重复:

      static final int hash(Object key) {
          int h;
          return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
      }
      
    • 高位参与运算:减少哈希冲突。

  2. 索引计算

    • 利用位运算替代取模(要求数组长度为2的幂):

      index = hash(key) & (capacity - 1); // 等价于 hash % capacity
      

三、哈希冲突解决

  1. 链表法(Java 8 之前):

    • 冲突的键值对以链表形式存储在同一个桶中。
    • 缺点:链表过长时查找效率低(O(n))。
  2. 红黑树法(Java 8+):

    • 当链表长度≥8,且数组容量≥64时,链表转为红黑树。
    • 优点:将查找时间优化至O(log n)。
    • 退化条件:红黑树节点数≤6时,退化为链表。

四、动态扩容(Rehashing)

  1. 触发条件

    • 元素数量 > 容量 × 负载因子(默认12 = 16×0.75)。
  2. 扩容流程

    • 新容量 = 旧容量 × 2(保持2的幂次)。
    • 遍历旧数组,重新计算每个键的索引并迁移到新数组。
    • 注意:多线程并发扩容可能导致死循环(链表成环)。
  3. 性能优化

    • 预分配足够容量,避免频繁扩容。
    • 示例:若预计存1000个元素,应初始化容量为2048(1000/0.75≈1333,取最近的2的幂次)。

五、关键操作流程

  1. 添加元素(put()

    • 计算键的哈希值及索引。

    • 若桶为空,直接插入新节点。

    • 若桶为链表/树,遍历查找是否存在相同键:

      • 存在则更新值。
      • 不存在则追加节点。
    • 检查扩容。

  2. 获取元素(get()

    • 计算索引,遍历链表或树查找键。
  3. 删除元素(remove()

    • 定位到桶,移除对应节点。
    • 若为红黑树,调整结构。

六、线程安全问题

  • 问题:多线程操作可能导致数据覆盖、死循环。

  • 解决方案

    • 使用 ConcurrentHashMap(分段锁或CAS机制)。
    • 用 Collections.synchronizedMap() 包装(性能较低)。

七、键对象的约束

  1. 重写 hashCode() 和 equals()

    • 若 equals() 返回 true,则 hashCode() 必须相同。
    • 反例:未正确重写导致重复键被误存。
  2. 示例

    class Key {
        String id;
    
        @Override
        public int hashCode() {
            return id.hashCode();
        }
    
        @Override
        public boolean equals(Object obj) {
            return obj instanceof Key && ((Key) obj).id.equals(id);
        }
    }
    

八、性能优化技巧

  1. 选择合适的初始容量

    // 预计存储2000个元素,初始容量计算:
    int initialCapacity = (int) (2000 / 0.75) + 1; // 2667 → 4096(2^12)
    HashMap<String, Integer> map = new HashMap<>(initialCapacity);
    
  2. 避免频繁修改:批量操作使用 putAll()

  3. 键对象不可变:防止哈希值变化导致定位错误。


九、总结

HashMap 设计核心

  • 哈希函数分散键,数组存储桶。
  • 链表与红黑树平衡冲突处理效率。
  • 动态扩容保持低负载因子。

适用场景

  • 高频查询、低频增删。
  • 单线程环境或需高并发时改用 ConcurrentHashMap

口诀
「哈希表里数组存,扰动函数算索引
链表红黑解冲突,扩容翻倍性能稳
键重哈希和等值,线程安全要谨慎!」