你真的理解 equals() 和 hashCode() 吗?Map 正确使用指南

Java 中的 Map(如 HashMap、LinkedHashMap、ConcurrentHashMap)底层依赖于对象的 equals() 和 hashCode() 方法来进行 键的唯一性判定。这两个方法若理解不清,很容易埋下严重的业务 Bug。


一、hashCode() 是定位,equals() 是确认

在 HashMap 插入过程中,内部大致执行如下逻辑:

int hash = key.hashCode();
int index = hash % table.length;

然后遍历该槽位上的链表或红黑树节点,依次调用:

if (key.equals(existingKey)) {
    // 覆盖旧值
}

即:

  • hashCode() 决定键应该进入哪个桶(bucket);

  • equals() 决定在桶中是否存在逻辑相同的键。


二、hashCode 与 equals 的“契约规则”

必须同时重写equals() 与 hashCode(),并且遵循以下约定:

  • 一致性(Consistent): 多次调用返回结果不变;

  • 等价性约定:

    若 a.equals(b) == true,则 a.hashCode() == b.hashCode();

  • 反之不要求:

    a.hashCode() == b.hashCode() 不一定 a.equals(b)。

违约的后果:

  • HashMap 查找失败;

  • Set 不能正确去重;

  • 多线程场景中可能产生“丢键”现象。


三、自定义对象常见错误示例

class Person {
    String name;
    int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 忘记重写 equals 和 hashCode
}
Set<Person> set = new HashSet<>();
set.add(new Person("Tom", 18));
set.add(new Person("Tom", 18));
System.out.println(set.size()); // 输出 2,而不是 1

📌 原因: 两个对象虽然字段相同,但在内存中地址不同,默认 equals() 为 Object.equals(),结果为 false。


四、推荐写法

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
    return Objects.hash(name, age);
}

👉 使用 Objects.hash() 是一种简洁安全的写法,推荐 Java 8+ 版本使用。


五、Map 使用最佳实践总结

场景

推荐做法

自定义 key

同时重写 equals() 和 hashCode()

使用 lombok

使用 @EqualsAndHashCode 注解

自定义规则复杂

手动实现逻辑,确保对称、传递、一致性

多线程环境(如 ConcurrentMap)

key 应为不可变类型,防止状态变更


六、进阶:hash 冲突与性能影响

hashCode() 若设计不合理,极易产生冲突:

public int hashCode() {
    return 1;
}

🔁 所有 key 会集中落入同一个 bucket,导致链表/红黑树退化为线性搜索,严重影响性能。

👉 推荐使用多个字段组合的方式生成 hash 值:

int result = name.hashCode();
result = 31 * result + Integer.hashCode(age);


七、典型问法:为什么 equals() 和 hashCode() 要一起重写?

答: 因为 Map(如 HashMap)要先通过 hashCode() 快速定位到某个桶,再通过 equals() 精确比较是否存在相等的 key。

  • 只重写 equals() 会导致无法命中 key;

  • 只重写 hashCode() 会误将不同对象当作同一个 key 处理。


八、hashCode 冲突测试与性能基准

在 HashMap 中,理想状态是不同的 key 能分散到不同的 bucket 中,形成一个“哈希均匀分布”。一旦冲突严重,性能会急剧下降。

🔬 测试示例:模拟 hashCode 冲突

class BadKey {
    private final int id;
    public BadKey(int id) {
        this.id = id;
    }
    @Override
    public int hashCode() {
        return 42; // 所有 key 都落在同一个桶
    }
    @Override
    public boolean equals(Object o) {
        return o instanceof BadKey && ((BadKey) o).id == this.id;
    }
}

💥 测试结果

Map<BadKey, String> map = new HashMap<>();
for (int i = 0; i < 10_000; i++) {
    map.put(new BadKey(i), "value");
}

在 JDK 8 前,大量 hash 冲突会使查询变成 O(n) 的链表遍历。

自 JDK 8 起,当冲突链表长度大于 8,自动转为红黑树,但仍不可取。

📌 性能建议

  • 保证 hashCode() 能有效区分不同 key;

  • 避免使用只基于一个字段的 hashCode(),特别是离散值较少的字段;

  • 使用多个字段组合,例如 Objects.hash(field1, field2);


九、Lombok 在 equals/hashCode 上的“坑”

Lombok 提供了便捷的 @EqualsAndHashCode 注解,但你必须清楚它默认的行为。

示例:

@Data
public class User {
    private String name;
    private int age;
}

等价于:

@EqualsAndHashCode
@Getter @Setter @ToString
public class User { ... }

⚠️ 可能的问题:

1. 包含父类字段:

默认会使用所有字段 + 父类字段做 equals/hashCode,有时不合理。

✅ 可通过参数配置:@EqualsAndHashCode(callSuper = false)

2. 引用复杂对象字段:

会递归调用 equals/hashCode,可能造成栈溢出或性能开销。

✅ 可以通过 exclude 或 onlyExplicitlyIncluded 控制:

@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Person {
    @EqualsAndHashCode.Include
    private String id;
    private Address address; // 不参与 hash/equals
}

3. IDE/反编译不易读:

生成的代码不显式展示,不便于调试和回溯。

建议:

  • 谨慎使用 Lombok 生成 equals/hashCode();

  • 特别在做为集合类 key 时,建议手写代码,逻辑更清晰;


十、实战误用案例剖析

让我们来看几个典型的“踩坑”场景,帮助你在实际开发中避坑:

1. 场景:HashMap 的 key 是可变对象

class Product {
    String id;
    String name;
    // equals/hashCode 用 id 计算
}
Product p = new Product("001", "A");
map.put(p, "value");
// 修改 id
p.id = "002";
System.out.println(map.get(p)); // ❌ null,无法命中

原因: 修改了参与 hashCode() 计算的字段,导致无法定位原桶。

✅ 正确做法:Map 的 key 应该是不可变对象


2. 场景:List.contains(Object) 无法命中

List<Person> list = new ArrayList<>();
list.add(new Person("Tom", 18));
list.contains(new Person("Tom", 18)); // ❌ false

原因: Person 没有重写 equals(),默认按引用比较。

✅ 正确做法:自定义对象参与集合操作,一定要重写 equals()/hashCode()。


3. 场景:Set 去重失败

Set<Point> points = new HashSet<>();
points.add(new Point(1, 2));
points.add(new Point(1, 2));
System.out.println(points.size()); // ❌ 输出 2

原因: Point 没有重写 equals/hashCode,两个对象 hash 不同。

✅ 正确写法:

@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }


十一、总结

  • equals() 和 hashCode() 不只是语法细节,而是 Java 集合框架正常工作的“基石”;

  • 想让 Map、Set 等容器行为正确,务必合理设计这两个方法;

  • 避免常见误区,尤其是在集合中放入自定义对象做 key 时

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小健学 Java

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值