1. 引言

HashMap 是一种强大的数据结构,在需要快速查找的场景中应用广泛。但如果不注意细节,它的性能可能并不理想。

本篇文章将带你了解如何让 HashMap 的性能达到最优。

2. HashMap 的性能瓶颈

HashMap 元素查找的时间复杂度为常数级别(O(1)),这得益于哈希函数的强大能力。

对于每个元素,HashMap 都会计算其哈希值,并将该元素放入与该哈希值对应的桶中。由于不同的对象可能具有相同的哈希值(即哈希冲突),桶的大小可能会增长。

⚠️ 桶本质上是一个链表。虽然链表查找效率不高(O(n)),但如果链表很短,问题不大。一旦出现大量哈希冲突,就会导致少数几个桶变得非常大,严重影响性能。

在最坏的情况下,所有元素都落入同一个桶中,此时 HashMap 就退化成了链表。 查找时间复杂度从 O(1) 直接降级为 O(n),性能急剧下降。

3. 使用红黑树替代链表

从 Java 8 开始,HashMap 引入了一个内置优化:当桶中的链表长度超过一定阈值时,会自动转换为红黑树。

这使得最坏情况下的查找时间复杂度从 O(n) 降低到 O(log n),性能提升明显。

⚠️ 但要注意:要使这个优化生效,HashMap 的键必须实现 Comparable 接口。

虽然这是个自动优化机制,但仍有不足之处:

  • O(log n) 还是不如理想的 O(1)
  • 构造和维护红黑树需要额外的计算资源和内存

4. 编写高质量的 hashCode 方法

设计哈希函数时,需关注两个关键因素:

  1. 哈希码的质量(即分布是否均匀)
  2. 哈希算法的执行速度

4.1. 衡量 hashCode 质量的标准

由于哈希值存储在 int 类型变量中,因此哈希值的数量是有限的。这也意味着,在不发生冲突的前提下,HashMap 能容纳的键数量是有限的。

为了尽可能避免哈希冲突,我们需要让哈希值分布尽可能均匀。 也就是说,每个哈希值出现的概率应该大致相同。

❌ 如果 hashCode 方法质量差,哈希值分布不均,甚至可能始终返回同一个值,这会导致严重的性能问题。

4.2. Object 默认的 hashCode 实现

一般不建议使用 Object 类默认的 hashCode 方法,因为通常我们不希望在 equals 方法中使用对象的引用地址进行比较(即对象身份)。

⚠️ 除非你确实需要以对象身份作为键,否则应提供自定义的 hashCode 实现。

4.3. 自定义 hashCode 实现

通常我们在重写 equals 方法时,也必须重写 hashCode 方法。

简单场景:对象身份仅由一个 id 决定

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    MemberWithId that = (MemberWithId) o;

    return id.equals(that.id);
}

@Override
public int hashCode() {
    return id;
}

✅ 这种方式非常高效,且不会产生哈希冲突。此时,HashMap 的行为几乎等同于使用 int 作为键。

复杂场景:多个字段参与比较

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    MemberWithIdAndName that = (MemberWithIdAndName) o;

    if (!id.equals(that.id)) return false;
    return name != null ? name.equals(that.name) : that.name == null;
}

此时,我们需要将多个字段的哈希值组合起来:

@Override
public int hashCode() {
    int result = id.hashCode();
    result = PRIME * result + (name != null ? name.hashCode() : 0);
    return result;
}

其中,PRIME 是一个精心挑选的质数。历史上最常用的是 31,原因如下:

  • 是质数,有助于均匀分布
  • 数值较小,便于优化
  • 可通过位运算优化乘法操作:
31 * i == (i << 5) - i

✅ 现代 JVM 会自动进行此类优化,因此我们无需手动写位运算来“炫技”。

当然,也可以使用更大的质数,比如 524287

524287 * i == i << 19 - i

可能带来更好的分布效果,减少冲突概率。

4.4. 使用 Objects.hash() 工具方法

上述算法非常经典,Java 提供了 Objects.hash() 方法供我们直接使用:

@Override
public int hashCode() {
    return Objects.hash(id, name);
}

✅ 内部实现正是我们前面提到的算法,乘数为 31

4.5. 使用更高质量的哈希函数

如果你对哈希质量有极高要求,而对性能不敏感,可以考虑使用 Guava 库提供的 Hashing 工具类:

@Override
public int hashCode() {
    HashFunction hashFunction = Hashing.murmur3_32();
    return hashFunction.newHasher()
      .putInt(id)
      .putString(name, Charsets.UTF_8)
      .hash().hashCode();
}

⚠️ 注意选择 32 位的哈希函数,因为 HashMap 无法存储更长的哈希值。

5. 总结

Java 中的 HashMap 是一个功能强大、高度优化的数据结构。但其性能表现,很大程度上取决于键对象的 hashCode 实现是否合理。

在本文中,我们探讨了如何编写高效、低冲突的哈希函数,从而让 HashMap 达到最佳性能。

📌 本文所有代码示例均可在 GitHub 上获取。


原始标题:Optimizing HashMap’s Performance