当前位置: 首页 > 技术教程

java为什么重写equals还要重写hashcode java要重写hashcode方法

  在 Java 开发中,equals 方法与 hashCode 方法是 Object 类的两个核心方法,二者存在紧密的约定关系。许多开发者仅重写 equals 方法却忽略 hashCode 方法,导致程序在使用集合框架(如 HashMap、HashSet)时出现逻辑错误。下面从方法约定、问题场景、重写原则三个维度,解析为何重写 equals 必须同步重写 hashCode,以及重写 hashCode 的核心意义。

  一、先明确:Object 类中 equals 与 hashCode 的原生约定

  Java 官方对 equals 与 hashCode 方法有明确的规范约定,这是重写二者的根本依据,核心约定有两点:

  一致性约定:若两个对象通过 equals 方法比较返回 true(即逻辑上相等),则这两个对象的 hashCode 方法必须返回相同的整数结果;

  反向约束:若两个对象的 hashCode 方法返回相同的整数结果,这两个对象通过 equals 方法比较不一定返回 true(即 hashCode 相同不代表对象逻辑相等,但 equals 相等则 hashCode 必须相同)。

  Object 类的原生实现中,equals 方法比较的是对象的内存地址(即==运算符的效果),hashCode 方法返回的是对象内存地址对应的哈希值。这种实现天然满足上述约定 —— 内存地址相同的对象(==为 true),equals 为 true 且 hashCode 相同;内存地址不同的对象,equals 为 false,hashCode 大概率不同(存在哈希碰撞,但符合约定)。

  但当开发者重写 equals 方法,改变 “对象相等” 的判断逻辑(如通过属性值判断相等,而非内存地址)时,若不同步重写 hashCode 方法,就会打破官方约定,引发后续问题。

360截图20250425224758032.jpg

  二、不重写 hashCode 的后果:集合框架中的逻辑错误

  重写 equals 却不重写 hashCode,最直接的问题会出现在依赖 hashCode 的集合类中,如 HashMap、HashSet、HashTable 等。这些集合基于 “哈希表” 数据结构实现,核心逻辑依赖 hashCode 定位对象存储位置,再通过 equals 确认对象唯一性,一旦 hashCode 不符合约定,集合功能会完全失效。

  1. 案例 1:HashSet 无法去重

  HashSet 的核心特性是 “存储不重复对象”,判断对象是否重复的逻辑为:

  先通过对象的 hashCode 方法获取哈希值,定位到哈希表中的 “桶位置”;

  若该桶位置为空,直接存储对象;

  若桶位置已有对象,通过 equals 方法逐一比较桶内对象与新对象,若 equals 为 true 则判定重复,不存储;若均为 false 则存储新对象。

  假设定义一个 User 类,重写 equals 方法(通过 id 属性判断相等),但未重写 hashCode:

  java取消自动换行复制

  class User {

  private Integer id;

  private String name;

  // 重写equals:id相同则对象相等

  @Override

  public boolean equals(Object o) {

  if (this == o) return true;

  if (o == null || getClass() != o.getClass()) return false;

  User user = (User) o;

  return id.equals(user.id);

  }

  // 未重写hashCode,使用Object原生实现

  // getter、setter、构造器省略

  }

  此时创建两个 id 相同的 User 对象,存入 HashSet:

  java取消自动换行复制

  public class Test {

  public static void main(String[] args) {

  User u1 = new User(1, "张三");

  User u2 = new User(1, "李四");

  HashSet<User> set = new HashSet<>();

  set.add(u1);

  set.add(u2);

  System.out.println(set.size()); // 输出2,而非预期的1

  }

  }

  问题原因:u1 与 u2 的 id 相同,equals 返回 true(逻辑相等),但因未重写 hashCode,Object 原生 hashCode 返回的是二者不同的内存地址哈希值,导致 HashSet 将它们定位到不同的桶位置,后续未执行 equals 比较就直接存储,最终 Set 中出现重复对象,违背去重特性。

  2. 案例 2:HashMap 无法正确获取值

  HashMap 存储键值对时,通过键的 hashCode 定位存储桶,再通过 equals 确认键的唯一性。若键对象重写 equals 却不重写 hashCode,会导致 “存得进,取不出” 的问题:

  java取消自动换行复制

  public class Test {

  public static void main(String[] args) {

  User u1 = new User(1, "张三");

  User u2 = new User(1, "李四");

  HashMap<User, String> map = new HashMap<>();

  map.put(u1, "用户1"); // 存入u1作为键

  // 用u2获取值,预期获取"用户1",实际返回null

  System.out.println(map.get(u2)); // 输出null

  }

  }

  问题原因:put 操作时,u1 的 hashCode 定位到桶 A;get 操作时,u2 的 hashCode(与 u1 不同)定位到桶 B,桶 B 中无对应键,因此返回 null。尽管 u1 与 u2 equals 为 true(逻辑上是同一个键),但因 hashCode 不同,HashMap 无法找到正确的存储位置,导致获取失败。

  这些案例证明:重写 equals 后若不重写 hashCode,会打破 Java 的方法约定,导致依赖 hashCode 的集合类完全无法正常工作,这是必须同步重写的核心原因。

  三、重写 hashCode 的核心原则:满足约定,减少碰撞

  重写 hashCode 并非随意返回一个整数,需遵循两个核心原则,确保既满足官方约定,又能提升集合操作效率:

  1. 原则 1:equals 相等的对象,hashCode 必须相等

  这是最根本的原则,直接对应官方约定。重写时需基于 equals 方法中用于判断相等的 “关键属性” 计算 hashCode——equals 比较哪些属性,hashCode 就必须依赖这些属性,确保属性值相同的对象(equals 为 true),hashCode 计算结果一致。

  例如,User 类 equals 基于 id 判断,hashCode 就应仅依赖 id:

  java取消自动换行复制

  @Override

  public int hashCode() {

  return id.hashCode(); // 直接使用id的hashCode,确保id相同则hashCode相同

  }

  若 equals 同时基于 id 和 name 判断,hashCode 则需结合二者:

  java取消自动换行复制

  @Override

  public boolean equals(Object o) {

  // 省略其他判断...

  User user = (User) o;

  return id.equals(user.id) && name.equals(user.name); // 基于id和name判断相等

  } 

  @Override

  public int hashCode() {

  // 结合id和name计算hashCode,确保二者都相同则hashCode相同

  return Objects.hash(id, name);

  }

  2. 原则 2:尽量减少哈希碰撞,提升集合效率

  哈希碰撞指 “equals 为 false 的对象,hashCode 却相同” 的情况(这并不违反官方约定,但会影响集合性能)。哈希碰撞越多,集合在桶内执行 equals 比较的次数就越多,操作效率越低。

  重写时可通过以下方式减少碰撞:

  结合多个属性计算:若仅依赖单个属性易碰撞(如属性值范围小,如性别只有男女),可结合多个属性(如 id + 性别 + 年龄),通过Objects.hash(属性1, 属性2, ...)计算,该方法会自动组合各属性的 hashCode,降低碰撞概率;

  避免返回固定值:若 hashCode 直接返回 1(或固定整数),所有对象都会被定位到同一个桶,集合退化为链表,查询效率从 O (1) 降至 O (n),完全失去哈希表的优势;

  利用现有 hashCode 实现:对于引用类型属性(如 String、Integer),直接使用其 hashCode 方法(这些类的 hashCode 实现已优化,碰撞率低);对于基本类型,可通过包装类(如 Integer.valueOf (int))转换后再获取 hashCode。

  四、equals 与 hashCode 是 “绑定关系”

  Java 中 equals 与 hashCode 并非独立方法,而是存在强绑定的约定关系 —— 重写 equals 改变对象相等的判断逻辑后,必须同步重写 hashCode,确保 “equals 相等则 hashCode 相同”,否则会导致 HashMap、HashSet 等核心集合类逻辑错误。

  重写 hashCode 时,需基于 equals 的关键属性计算,既满足约定,又尽量减少哈希碰撞。实际开发中,可借助Objects.hash()方法快速实现(该方法已处理 null 值,避免空指针异常),无需手动编写复杂的哈希计算逻辑,高效且不易出错。

  理解二者的协同关系,是编写符合 Java 规范、逻辑严谨的代码的基础,也是避免集合操作 bug 的关键。

 


猜你喜欢