在 Java 中,hashCode() 方法与 equals() 方法,在对象存入集合(尤其是 HashSet、HashMap 这类基于哈希表的集合)时,扮演着至关重要的协作角色。
关于这两个方法的官方约定规则(比如 “若两个对象 equals() 结果为 true,则它们的 hashCode() 必须相等” 等核心规范),在 JavaDoc 中有最精准、完整的定义。
而在这里,我会先聚焦于它们的核心作用与适用场景 —— 帮助你先理解 “为什么需要正确实现这两个方法”,进而更清晰地认识到规范实现它们的重要性。
equals() 在大多数集合中用于确定集合是否包含某个给定元素。例如:
package com.hxstrive.java_collection; import java.util.ArrayList; import java.util.List; public class EqualsDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("123"); boolean contains123 = list.contains("123"); System.out.println("是否包含'123'元素?" + contains123); //是否包含'123'元素?true } }
上述代码中,ArrayList 会遍历其所有元素,并执行 "123".equals(element) 来判断该元素是否与参数对象 "123" 相等。正是 String.equals() 的实现决定了两个字符串是否相等。类似如下代码:
package com.hxstrive.java_collection; import java.util.ArrayList; import java.util.List; public class EqualsDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("123"); boolean contains123 = false; for(String str : list) { // 这里 if("123".equals(str)) { contains123 = true; break; } } System.out.println("是否包含'123'元素?" + contains123); //是否包含'123'元素?true } }
equals() 方法在移除元素时也会用到。例如:
List list = new ArrayList(); list.add("123"); boolean removed = list.remove("123");
当调用 ArrayList 的移除方法时,它会遍历内部所有元素,并通过执行 "123".equals(element) 的逻辑,判断每个元素是否与目标参数 "123" 相等。遍历过程中,第一个满足相等条件的元素会被找到并移除。
由此可见,若你自定义的类需要与 ArrayList 这类 Java 集合类协同工作(比如实现元素的查找、移除等功能),正确实现 equals() 方法是至关重要的前提 —— 错误的 equals() 逻辑会导致集合无法正确识别目标元素。
那么,如何才算 “正确地” 实现 equals() 呢?这首先要明确:两个对象何时算 “相等”,并没有统一的标准答案,而是取决于你的应用场景、类的设计目的,以及具体要实现的业务逻辑。
举个例子:假设你需要处理从数据库加载的 Employee(员工)对象,判断两个 Employee 是否相等时,可能会以 “员工编号” 为唯一依据(只要编号相同,就认为是同一个员工);但在另一场景下,也可能需要同时比较 “姓名 + 部门” 等多个属性 —— 具体逻辑需结合实际需求定义。例如:
public class Employee { protected long employeeId; // 员工编号 protected String firstName; // 员工名 protected String lastName; // 员工姓 }
你可以根据业务需求定义 Employee 对象的相等规则:比如,仅以 “员工编号(employeeId)” 为判定依据 —— 只要两个 Employee 的 employeeId 相同,就认为它们相等;也可以要求 “所有字段完全匹配”—— 必须 employeeId、firstName(名)、lastName(姓)均相同,才判定为相等。
下面为你提供两种符合上述规则的 equals() 方法实现示例:
package com.hxstrive.java_collection.equals_hashcode; import java.util.ArrayList; import java.util.List; public class Employee { protected long employeeId; // 员工编号 protected String firstName; // 员工名 protected String lastName; // 员工姓 public Employee(long employeeId, String firstName, String lastName) { this.employeeId = employeeId; this.firstName = firstName; this.lastName = lastName; } @Override public boolean equals(Object obj) { if(obj == null) { return false; } if(!(obj instanceof Employee)) { return false; } Employee other = (Employee) obj; return this.employeeId == other.employeeId; } public static void main(String[] args) { List<Employee> list = new ArrayList<>(); list.add(new Employee(1000L, "first1", "last1")); list.add(new Employee(2000L, "first2", "last2")); boolean isContains = list.contains(new Employee(2000L, "", "")); System.out.println(isContains); // true } }
或者
package com.hxstrive.java_collection.equals_hashcode; import java.util.ArrayList; import java.util.List; public class Employee { protected long employeeId; // 员工编号 protected String firstName; // 员工名 protected String lastName; // 员工姓 public Employee(long employeeId, String firstName, String lastName) { this.employeeId = employeeId; this.firstName = firstName; this.lastName = lastName; } @Override public boolean equals(Object obj) { if(obj == null) { return false; } if(!(obj instanceof Employee)) { return false; } Employee other = (Employee) obj; if(this.employeeId != other.employeeId) return false; if(!this.firstName.equals(other.firstName)) return false; if(!this.lastName.equals(other.lastName)) return false; return true; } public static void main(String[] args) { List<Employee> list = new ArrayList<>(); list.add(new Employee(1000L, "first1", "last1")); list.add(new Employee(2000L, "first2", "last2")); boolean isContains = list.contains(new Employee(2000L, "", "")); System.out.println(isContains); // false } }
这两种 equals() 实现究竟哪一种 “合适”,核心取决于你的具体业务场景与需求,没有绝对统一的答案。
比如,当你需要从缓存中查找某个 Employee 对象时,通常只需通过唯一标识(employeeId)就能定位目标 —— 这种场景下,“仅判断 employeeId 相等” 的实现就足够,既简洁又能满足需求;
但在另一些场景中,你可能需要更严格的相等判定:例如,要检查一个 Employee 对象的副本是否与原对象完全一致(未发生任何字段变更),这时就需要验证 employeeId、firstName、lastName 等所有字段是否均相等,“全字段比较” 的实现才符合要求。
点击查看更多:https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#equals-java.lang.Object-
对象的 hashCode() 方法,在将对象插入 Hashtable、HashMap 或 HashSet 这类基于哈希表的集合时,会起到关键作用。如果你对哈希表的内部工作机制尚不了解,建议先了解相关基础知识,这会帮助你更好地理解 hashCode() 的用途。
下图是 HashMap 内部存储结构,来自互联网:
具体来说,向哈希表中插入对象时,会依赖一个“键(Key)”:首先计算该键的哈希码(通过 hashCode() 方法),哈希表会根据这个哈希码,确定对象在内部存储结构中的具体位置;而当你需要从哈希表中查找某个对象时,同样需要提供对应的键 —— 此时会再次计算该键的哈希码,进而快速定位到可能存储目标对象的位置,大幅减少遍历查找的成本。
在哈希表内部,键的哈希码(由 hashCode() 生成)仅用于定位到一个特定的 “存储区域”—— 这个区域也常被称为 “桶”(Bucket)或 “列表”。
需要注意的是,不同的键对象可能生成相同的哈希码(这种情况称为 “哈希冲突”),因此仅通过哈希码无法直接确定找到的是正确的键。此时,哈希表会遍历该区域内所有具有相同哈希码的键,再通过调用键的 equals() 方法逐一比对,最终找到与目标完全匹配的键。一旦匹配成功,就会返回该键所关联存储的对象。
由此可见,在哈希表中完成对象的存储与查找操作时,hashCode() 负责 “快速定位存储区域”,equals() 负责 “精准匹配目标键”,二者是协同工作、缺一不可的。
若想让 Java 集合 API 中的哈希表(如 HashMap、HashSet、Hashtable)正常工作,在自定义类中实现 hashCode() 方法时,有两条核心规则必须了解并遵守:
(1)如果根据 object1 和 object2 的 equals() 方法判断它们是相等的,那么它们也必须具有相同的哈希码。
(2)如果 object1 和 object2 具有相同的哈希码,它们不一定也相等。
简而言之:
(1) 如果相等,那么也具有相同的哈希码。
(2)相同的哈希码不能保证相等。
以下是与前文所示 equals() 方法实现逻辑相匹配的两个 hashCode() 方法示例:
package com.hxstrive.java_collection.equals_hashcode; import java.util.HashMap; import java.util.Map; public class Employee { protected long employeeId; // 员工编号 protected String firstName; // 员工名 protected String lastName; // 员工姓 public Employee(long employeeId, String firstName, String lastName) { this.employeeId = employeeId; this.firstName = firstName; this.lastName = lastName; } // 如果两个对象的 employeeId 相等,则认为相等 @Override public boolean equals(Object obj) { if(obj == null) { return false; } if(!(obj instanceof Employee)) { return false; } Employee other = (Employee) obj; return this.employeeId == other.employeeId; } @Override public int hashCode(){ // 直接使用员工ID作为 hashCode // 满足 equals 相等,hashCode 也相等的约束 return (int) employeeId; } // 演示 hashCode 相等,但是 equals 不相等 public static void main(String[] args) { Employee employee1 = new Employee(4294968296L, "first1", "last1"); Employee employee2 = new Employee(8589935592L, "first2", "last2"); System.out.println("employee1.hashCode=" + employee1.hashCode()); //employee1.hashCode=1000 System.out.println("employee2.hashCode=" + employee2.hashCode()); //employee2.hashCode=1000 System.out.println(employee1.hashCode() == employee2.hashCode()); //true System.out.println(employee1.equals(employee2)); //false } }
或者
public class Employee { protected long employeeId; protected String firstName; protected String lastName; public int hashCode(){ // 使用多个字段的 hashCode 计算出一个新的 hashCode return (int) employeeId * firstName.hashCode() * lastName.hashCode(); } }
需要注意的是:若两个 Employee 对象通过 equals() 判定为相等,它们的 hashCode() 结果必须相同—— 这是遵循 Java 哈希约定的核心要求。但反过来则不成立,正如第一个示例中很容易看出的:两个 Employee 对象即使 equals() 判定为不相等,它们的 hashCode() 仍可能相同(即发生 “哈希冲突”)。
在这两个示例里,hashCode() 的计算逻辑都是将 employeeId 向下取整为整数。这意味着,即便多个不同的员工 ID会生成相同的哈希码,但这些 Employee 对象依然不会被判定为相等 —— 因为它们的原始员工 ID(employeeId)本身并不相同,而 equals() 方法正是以 employeeId 作为核心判定依据的。
点击查看更多:https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#hashCode--