【Java踩坑笔记】【基础语法篇】05_重写equals不重写hashCode会怎样?

📅 2026/6/26 1:19:58
【Java踩坑笔记】【基础语法篇】05_重写equals不重写hashCode会怎样?
摘要你重写了equals却没重写hashCode然后发现HashMap.get()怎么也取不到值。这不是玄学这是约定。一、问题现象classUser{privateLongid;privateStringname;publicUser(Longid,Stringname){this.idid;this.namename;}Overridepublicbooleanequals(Objecto){if(thiso)returntrue;if(onull||getClass()!o.getClass())returnfalse;Useruser(User)o;returnObjects.equals(id,user.id)Objects.equals(name,user.name);}// ❌ 没有重写 hashCode}publicclassHashMapTest{publicstaticvoidmain(String[]args){Useru1newUser(1L,Alice);Useru2newUser(1L,Alice);// 逻辑上相等的另一个对象System.out.println(u1.equals(u2));// trueequals 判断相等MapUser,StringmapnewHashMap();map.put(u1,VIP);System.out.println(map.get(u1));// VIP能取到u1 是同一个对象System.out.println(map.get(u2));// null ❌取不到}}运行结果true VIP nullu1.equals(u2) true但map.get(u2)返回null—— 这就是不重写hashCode的典型灾难。二、踩坑现场场景 1用自定义对象做 Map 的 Key// ❌ 常见错误用 DTO 做 key但没重写 hashCodeclassOrderQuery{privateLonguserId;privateDatestartDate;privateDateendDate;// 只重写了 equals没重写 hashCode}MapOrderQuery,ListOrdercachenewHashMap();OrderQueryquerynewOrderQuery(1L,start,end);cache.put(query,orders);// 换个新的 query 对象字段相同取不出来OrderQuerysameQuerynewOrderQuery(1L,start,end);cache.get(sameQuery);// null ❌场景 2HashSet重复添加元素SetUserusersnewHashSet();users.add(newUser(1L,Alice));users.add(newUser(1L,Alice));// equals 相等但 hashCode 不同System.out.println(users.size());// 2 ❌ 应该是 1三、原理解析3.1hashCode的约定Object 规范Java 语言规范对hashCode有明确约定同一个对象未修改多次调用hashCode必须返回相同的值如果两个对象equals返回true它们的hashCode必须相等建议如果两个对象equals返回false它们的hashCode尽量不同减少 hash 冲突第 2 条是核心equals相等 →hashCode必须相等。3.2HashMap的查找流程map.get(key) │ ▼ 先计算 key.hashCode() → 找到桶bucket位置 │ ▼ 遍历桶内的所有元素用 equals() 判断是否相等 │ ├── 找到 → 返回值 └── 没找到 → 返回 null问题所在u1.hashCode()101// 假设在桶 101u2.hashCode()205// 假设在桶 205因为没重写默认是对象地址算出来的map.get(u2):计算 hashCode205去桶205找 → 桶205是空的u1 在桶101 返回null❌3.3 默认hashCode的实现Object的默认hashCode是基于对象内存地址计算的JVM 实现相关不同对象的hashCode几乎一定不同。// Object 的 hashCode 是 native 方法publicnativeinthashCode();3.4equals和hashCode的不变量equals 相等 → hashCode 必须相等 ✅必须遵守 equals 不等 → hashCode 可以相等 ✅允许 hash 冲突 hashCode 相等 → equals 可以不等 ✅允许 hash 冲突 hashCode 不等 → equals 必须不等 ❌不会 happened因为 HashMap 先比 hashCode四、正确写法4.1 同时重写equals和hashCodeclassUser{privateLongid;privateStringname;// 构造方法省略...Overridepublicbooleanequals(Objecto){if(thiso)returntrue;if(onull||getClass()!o.getClass())returnfalse;Useruser(User)o;returnObjects.equals(id,user.id)Objects.equals(name,user.name);}OverridepublicinthashCode(){returnObjects.hash(id,name);// ✅ 用 Objects.hash 自动处理 null}}4.2 用 Lombok 自动生成推荐importlombok.EqualsAndHashCode;EqualsAndHashCodeclassUser{privateLongid;privateStringname;}或只生成特定字段EqualsAndHashCode(of{id})// 只用 id 判断相等classUser{privateLongid;privateStringname;privateIntegerage;// 不参与 equals/hashCode}4.3 用 IDE 自动生成IDEA右键 → Generate → equals() and hashCode()生成效果推荐选择Objects.equals/Objects.hash模板Overridepublicbooleanequals(Objecto){if(thiso)returntrue;if(onull||getClass()!o.getClass())returnfalse;Useruser(User)o;returnObjects.equals(id,user.id)Objects.equals(name,user.name);}OverridepublicinthashCode(){returnObjects.hash(id,name);}4.4 只读对象用recordJava 16// ✅ record 自动生成 equals hashCode toStringpublicrecordUser(Longid,Stringname){}// 不需要手动重写任何方法五、最佳实践✅ 5 条铁律只要重写了equals必须同时重写hashCodeequals用到的字段hashCode也必须用到保持一致**不可变对象的hashCode可以缓存见下方优化**用 LombokEqualsAndHashCode或让 IDE 生成不要手写hashCode返回int要注意哈希冲突概率 性能优化hashCode缓存classUser{privatefinalLongid;privatefinalStringname;privateinthashCode;// 缓存 hashCodepublicUser(Longid,Stringname){this.idid;this.namename;}Overridepublicbooleanequals(Objecto){if(thiso)returntrue;if(onull||getClass()!o.getClass())returnfalse;Useruser(User)o;returnObjects.equals(id,user.id)Objects.equals(name,user.name);}OverridepublicinthashCode(){if(hashCode0){// 懒加载缓存hashCodeObjects.hash(id,name);}returnhashCode;}}前提对象是不可变的字段用final否则hashCode缓存会失效。️ 阿里巴巴 Java 开发手册规约【强制】因为Set存储的是不重复的对象并且依据hashCode和equals进行判断所以Set存储的对象必须重写这两个方法。【强制】如果自定义对象作为Map的键那么必须重写equals和hashCode。六、小结equals相等 →hashCode必须相等这是HashMap/HashSet正确工作的前提不重写hashCode会导致逻辑相等的对象在HashMap里找不到永远同时重写equals和hashCode用Objects.equalsObjects.hashLombok 的EqualsAndHashCode是最省心的方案Java 16 的record天然正确适合作为 DTO/值对象下一篇预告try-finally 里的 return到底返回谁—— finally 块里的 return 会覆盖 try 里的返回值这个坑比你想的更隐蔽。