一句话说透Java里面的HashMap如何解决散列碰撞

254 阅读2分钟

哈希碰撞就像 不同的人被分配到同一个储物柜(即不同的键被哈希到同一个数组位置)。HashMap 用两种方法解决碰撞:


一、核心方法:链表 + 红黑树

1. 链表法(Java 8 之前)
  • 操作方式

    • 当多个键被哈希到同一个桶(数组位置)时,这些键值对会以链表形式存储。
    • 插入新元素时,直接追加到链表末尾。
    • 查找元素时,遍历链表逐个比较键的哈希值和内容(equals())。
  • 示例

    // 假设键 "A" 和键 "B" 哈希到同一位置  
    map.put("A", 1);  
    map.put("B", 2);  
    // 桶结构:["A=1""B=2"]  
    
2. 红黑树优化(Java 8+)
  • 触发条件

    • 当链表长度 ≥ 8,且数组容量 ≥ 64 时,链表转为红黑树。
    • 当红黑树节点数 ≤ 6 时,退化为链表。
  • 目的

    • 链表过长时查询慢(O(n)),转为红黑树(O(log n))提升效率。
  • 示例

    // 插入8个哈希冲突的键  
    for (int i=0; i<8; i++) {  
        map.put("Key" + i, i);  
    }  
    // 桶结构:红黑树(平衡二叉搜索树)  
    

二、如何减少哈希碰撞?

  1. 优化哈希函数

    • 扰动算法:对键的 hashCode() 进行高位运算,减少低位重复。

      static final int hash(Object key) {  
          int h;  
          return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  
      }  
      
    • 自定义对象需重写 hashCode() 和 equals()

      class User {  
          String name;  
          int age;  
          @Override  
          public int hashCode() {  
              return Objects.hash(name, age); // 合并多个字段的哈希值  
          }  
      }  
      
  2. 动态扩容

    • 当元素数量超过 容量 × 负载因子(默认0.75)时,数组扩容至2倍,重新分配键值对。
    • 扩容后哈希冲突概率降低:哈希值计算时用更大的数组长度取模。

三、碰撞解决全流程

  1. 计算哈希值:通过扰动函数得到键的哈希值。

  2. 定位桶位置index = hash & (capacity - 1)

  3. 处理碰撞

    • 若桶为空:直接插入新节点。
    • 若桶为链表:遍历链表,若发现相同键则替换值;否则追加到链表尾部。
    • 若桶为红黑树:按树结构插入或替换节点。
  4. 检查扩容或树化

    • 元素数量超阈值 → 扩容。
    • 链表长度≥8且容量≥64 → 转红黑树。

四、总结

方法优点缺点
链表法实现简单,小数据量高效链表过长时查询慢(O(n))
红黑树法大数据量查询快(O(log n))树结构维护成本高

口诀
「哈希碰撞不可怕,链表红黑来当家
扩容扰动降冲突,重写哈希是妙法
链表查询虽然慢,树化提速顶呱呱!」