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 时;