Jakob Jenkov 2019-05-25
hashCode() 和 equals() 方法在你将对象插入 Java 集合(Collections)时扮演着关键角色。这两个方法的具体契约规则最好参考官方 JavaDoc,本文主要解释它们的作用——即它们被用来做什么,以便你理解为何正确实现它们至关重要。
equals()
大多数集合类使用 equals() 方法来判断集合是否包含某个给定元素。例如:
List list = new ArrayList();
list.add("123");
boolean contains123 = list.contains("123");
ArrayList 会遍历其所有元素,并对每个元素执行 "123".equals(element) 来判断该元素是否等于参数对象 "123"。这里真正决定两个字符串是否相等的是 String.equals() 的实现。
equals() 方法在移除元素时也会被使用。例如:
List list = new ArrayList();
list.add("123");
boolean removed = list.remove("123");
同样地,ArrayList 会遍历所有元素并执行 "123".equals(element),找到第一个与参数 "123" 相等的元素后将其移除。
由此可见,如果你自己的类要与 Java 集合类良好协作,正确实现 equals() 是必不可少的。那么,如何“正确”地实现 equals() 呢?
两个对象何时相等? 这取决于你的应用程序、具体类以及你要实现的功能。
例如,假设你正在从数据库加载并处理 Employee 对象:
public class Employee {
protected long employeeId;
protected String firstName;
protected String lastName;
}
你可以决定:只要两个 Employee 对象的 employeeId 相同,它们就相等;或者你也可以要求所有字段(employeeId、firstName 和 lastName)都必须相等。
以下是两种符合上述不同标准的 equals() 实现示例:
示例 1:仅比较 employeeId
public class Employee {
// ...
public boolean equals(Object o) {
if (o == null) return false;
if (!(o instanceof Employee)) return false;
Employee other = (Employee) o;
return this.employeeId == other.employeeId;
}
}
示例 2:比较所有字段
public class Employee {
// ...
public boolean equals(Object o) {
if (o == null) return false;
if (!(o instanceof Employee)) return false;
Employee other = (Employee) o;
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;
}
}
哪种实现是“正确的”,完全取决于你的实际需求。有时你可能只需要通过 employeeId 从缓存中查找 Employee 对象;而在其他场景下(比如判断一个 Employee 对象副本是否发生了变化),你可能需要更严格的相等性判断。
hashCode()
当你将对象插入 HashTable、HashMap 或 HashSet 时,会用到对象的 hashCode() 方法。如果你不了解哈希表(hashtable)的内部原理,可以查阅 Java官方文档上关于哈希表的介绍。
向哈希表插入对象时,会使用一个“键”(key)。系统会计算该键的哈希码(hash code),并据此决定在内部何处存储该对象。当你需要从哈希表中查找对象时,同样提供一个键,系统再次计算其哈希码,并据此确定在哪个区域进行搜索。
需要注意的是:哈希码只指向内部的某个“区域”(如链表、桶等)。由于不同的键对象可能具有相同的哈希码,因此仅靠哈希码无法保证一定能找到正确的键。哈希表会在该区域内遍历所有具有相同哈希码的键,并使用键的 equals() 方法来精确匹配目标键。一旦找到正确的键,就返回其关联的对象。
因此,在哈希表中存储和查找对象时,实际上是 hashCode() 和 equals() 方法协同工作的结果。
实现 hashCode() 的两条重要规则
为了确保你的类能与 Java Collections API 中的哈希表类(如 HashMap、HashSet)正常工作,在实现 hashCode() 时应遵守以下两条规则:
- 如果两个对象根据
equals()方法判断为相等,那么它们的hashCode()必须返回相同的值。 - 如果两个对象的
hashCode()相同,它们不一定相等。
简而言之:
- 相等 ⇒ 哈希码相同
- 哈希码相同 ⇏ 相等
与前面 equals() 对应的 hashCode() 实现示例
示例 1:仅基于 employeeId
public class Employee {
protected long employeeId;
protected String firstName;
protected String lastName;
public int hashCode() {
return (int) employeeId;
}
}
示例 2:基于所有字段
public class Employee {
protected long employeeId;
protected String firstName;
protected String lastName;
public int hashCode() {
return (int) employeeId * firstName.hashCode() * lastName.hashCode();
}
}
注意:在这两个例子中,如果两个 Employee 对象相等,它们必然具有相同的哈希码。但反过来不成立——尤其在第一个例子中,多个不同的 employeeId(因强制转换为 int 而截断)可能产生相同的哈希码,但这些对象并不相等。