【Java踩坑笔记】【基础语法篇】05_重写equals不重写hashCode会怎样?
·
摘要:你重写了
equals,却没重写hashCode,然后发现HashMap.get()怎么也取不到值。这不是玄学,这是约定。
一、问题现象
class User {
private Long id;
private String name;
public User(Long id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id) &&
Objects.equals(name, user.name);
}
// ❌ 没有重写 hashCode!
}
public class HashMapTest {
public static void main(String[] args) {
User u1 = new User(1L, "Alice");
User u2 = new User(1L, "Alice"); // 逻辑上相等的另一个对象
System.out.println(u1.equals(u2)); // true(equals 判断相等)
Map<User, String> map = new HashMap<>();
map.put(u1, "VIP");
System.out.println(map.get(u1)); // "VIP"(能取到,u1 是同一个对象)
System.out.println(map.get(u2)); // null ❌(取不到!)
}
}
运行结果:
true
VIP
null
u1.equals(u2) == true,但 map.get(u2) 返回 null —— 这就是不重写 hashCode 的典型灾难。
二、踩坑现场
场景 1:用自定义对象做 Map 的 Key
// ❌ 常见错误:用 DTO 做 key,但没重写 hashCode
class OrderQuery {
private Long userId;
private Date startDate;
private Date endDate;
// 只重写了 equals,没重写 hashCode
}
Map<OrderQuery, List<Order>> cache = new HashMap<>();
OrderQuery query = new OrderQuery(1L, start, end);
cache.put(query, orders);
// 换个新的 query 对象(字段相同),取不出来
OrderQuery sameQuery = new OrderQuery(1L, start, end);
cache.get(sameQuery); // null ❌
场景 2:HashSet "重复"添加元素
Set<User> users = new HashSet<>();
users.add(new User(1L, "Alice"));
users.add(new User(1L, "Alice")); // equals 相等,但 hashCode 不同
System.out.println(users.size()); // 2 ❌ 应该是 1
三、原理解析
3.1 hashCode 的约定(Object 规范)
Java 语言规范对 hashCode 有明确约定:
- 同一个对象(未修改)多次调用
hashCode,必须返回相同的值 - 如果两个对象
equals返回true,它们的hashCode必须相等 - (建议)如果两个对象
equals返回false,它们的hashCode尽量不同(减少 hash 冲突)
第 2 条是核心:equals 相等 → hashCode 必须相等。
3.2 HashMap 的查找流程
map.get(key)
│
▼
先计算 key.hashCode() → 找到桶(bucket)位置
│
▼
遍历桶内的所有元素,用 equals() 判断是否相等
│
├── 找到 → 返回值
└── 没找到 → 返回 null
问题所在:
u1.hashCode() = 101 // 假设在桶 101
u2.hashCode() = 205 // 假设在桶 205(因为没重写,默认是对象地址算出来的)
map.get(u2):
计算 hashCode = 205
去桶 205 找 → 桶 205 是空的(u1 在桶 101)
返回 null ❌
3.3 默认 hashCode 的实现
Object 的默认 hashCode 是基于对象内存地址计算的(JVM 实现相关),不同对象的 hashCode 几乎一定不同。
// Object 的 hashCode 是 native 方法
public native int hashCode();
3.4 equals 和 hashCode 的不变量
equals 相等 → hashCode 必须相等 ✅(必须遵守)
equals 不等 → hashCode 可以相等 ✅(允许 hash 冲突)
hashCode 相等 → equals 可以不等 ✅(允许 hash 冲突)
hashCode 不等 → equals 必须不等 ❌(不会 happened,因为 HashMap 先比 hashCode)
四、正确写法
4.1 同时重写 equals 和 hashCode
class User {
private Long id;
private String name;
// 构造方法省略...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id) &&
Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name); // ✅ 用 Objects.hash 自动处理 null
}
}
4.2 用 Lombok 自动生成(推荐)
import lombok.EqualsAndHashCode;
@EqualsAndHashCode
class User {
private Long id;
private String name;
}
或只生成特定字段:
@EqualsAndHashCode(of = {"id"}) // 只用 id 判断相等
class User {
private Long id;
private String name;
private Integer age; // 不参与 equals/hashCode
}
4.3 用 IDE 自动生成
IDEA:右键 → Generate → equals() and hashCode()
生成效果(推荐选择 Objects.equals / Objects.hash 模板):
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id) && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
4.4 只读对象用 record(Java 16+)
// ✅ record 自动生成 equals + hashCode + toString
public record User(Long id, String name) {}
// 不需要手动重写任何方法
五、最佳实践
✅ 5 条铁律
- 只要重写了
equals,必须同时重写hashCode equals用到的字段,hashCode也必须用到(保持一致)**- 不可变对象的
hashCode可以缓存(见下方优化)** - 用 Lombok
@EqualsAndHashCode或让 IDE 生成,不要手写 hashCode返回int,要注意哈希冲突概率
🔍 性能优化:hashCode 缓存
class User {
private final Long id;
private final String name;
private int hashCode; // 缓存 hashCode
public User(Long id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id) && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
if (hashCode == 0) { // 懒加载缓存
hashCode = Objects.hash(id, name);
}
return hashCode;
}
}
前提:对象是不可变的(字段用
final),否则hashCode缓存会失效。
🛠️ 阿里巴巴 Java 开发手册规约
【强制】 因为
Set存储的是不重复的对象,并且依据hashCode和equals进行判断,所以Set存储的对象必须重写这两个方法。【强制】 如果自定义对象作为
Map的键,那么必须重写equals和hashCode。
六、小结
equals相等 →hashCode必须相等,这是HashMap/HashSet正确工作的前提- 不重写
hashCode会导致"逻辑相等"的对象在HashMap里找不到 - 永远同时重写
equals和hashCode,用Objects.equals+Objects.hash - Lombok 的
@EqualsAndHashCode是最省心的方案 - Java 16+ 的
record天然正确,适合作为 DTO/值对象
下一篇预告:try-finally 里的 return,到底返回谁? —— finally 块里的 return 会覆盖 try 里的返回值,这个坑比你想的更隐蔽。
更多推荐
所有评论(0)