1. 引言
我们如何比较对象
在 Java 中,对象不仅仅是数据,每个对象在内存中都有自己的地址。运算符 == 回答的是“这是同一个盒子吗?”这个问题,也就是说比较的是引用(地址),而不是内容。方法 equals 则用于比较内容。本质上,如果不重写,equals 的行为与 == 相同。
Person p1 = new Person("Ivan", 20);
Person p2 = new Person("Ivan", 20);
System.out.println(p1 == p2); // false — 这是内存中的不同对象!
要让两个不同的对象按数据被视为相等(例如所有重要字段都相同),需要重写 equals。并且如果你计划在哈希集合中使用这些对象,务必要与之配套正确地重写 hashCode。
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // 这是同一个对象
if (o == null || getClass() != o.getClass()) return false; // 检查类
Person person = (Person) o; // 转为所需类型
return age == person.age && name.equals(person.name); // 比较字段
}
@Override
public int hashCode() {
return Objects.hash(name, age); // 使其可用于 HashSet/HashMap
}
}
这种重写对于在 HashSet/HashMap 中的正确行为至关重要:没有正确的 equals 和 hashCode,集合会把数据相同的对象也当作不同的对象。
equals 中的等价关系取决于你的需求:可以按所有字段比较,也可以按部分字段(例如对 User 仅比较 email)——关键是保持一致并遵守契约。
在哪些场景尤其重要?
- 基于哈希表的集合:HashSet、HashMap、LinkedHashSet 等。
- 在集合中查找和删除元素:如果没有正确的 equals,目标对象可能“找不到”。
- 在业务逻辑中:例如两个拥有相同 email 的 User 应视为同一用户。
hashCode——它有什么用?
哈希集合(例如 HashSet、HashMap)使用哈希表。方法 hashCode 计算一个整数——“桶的地址”,对象将被放入该桶。如果两个对象按 equals 相等,那么它们的 hashCode 必须相同。违反这个规则——集合就会变得不可预测。
2. equals 和 hashCode 的契约
equals 的契约
对 equals 的基本要求:
- 自反性:a.equals(a) 始终为 true。
- 对称性:如果 a.equals(b) 为 true,那么 b.equals(a) 也为 true。
- 传递性:如果 a.equals(b) 且 b.equals(c),则 a.equals(c) 也为 true。
- 一致性:在对象未发生变化时,多次调用结果应稳定。
- 与 null 的比较:任何对象都不等于 null。
hashCode 的契约
- 如果两个对象按 equals 相等,它们的 hashCode 必须相等。
- 若对象不相等,它们的哈希码可能碰撞(允许但不理想)。
- 在对象逻辑未改变的情况下,其 hashCode 应保持不变。
换言之,相同的 hashCode 是相等的必要但非充分条件:相同哈希并不保证按 equals 相等。
3. 实现 equals 与 hashCode:示例
以 Person 类为例,其相等性由 name 与 age 字段决定。
public class Person {
private String name;
private int age;
// 构造器、getter、setter...
@Override
public boolean equals(Object o) {
if (this == o) return true; // 比较引用
if (o == null || getClass() != o.getClass()) return false; // 检查类
Person person = (Person) o; // 类型转换
// 比较字段
return age == person.age &&
(name != null ? name.equals(person.name) : person.name == null);
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age; // 31 是常用的素数选择
return result;
}
}
- 先进行快速检查:引用与类。
- 然后比较重要字段。
- 在 hashCode 中使用素数 31 以减少碰撞。
使用 Objects.equals 和 Objects.hash
自 Java 7 起,Objects 类可简化代码,并使其对 null 更加安全:
import java.util.Objects;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
4. equals、hashCode 与 compareTo:它们如何关联
equals 与 compareTo 如何关联?
接口 Comparable 定义了方法 compareTo,其返回负数/零/正数分别表示“小于/等于/大于”。通常希望由 a.compareTo(b) == 0 能推出 a.equals(b)。反之不必然成立。
如果破坏了一致性(例如 compareTo 只按年龄比较,而 equals 按姓名与年龄比较),那么诸如 TreeSet/TreeMap 的可排序集合可能会出现意外行为:从顺序的角度看对象“相等”,但从内容上并不相等。
集合中的 equals 与 hashCode
- 在 HashSet 和 HashMap 中,添加/查找/删除操作依赖于正确实现的 equals 与 hashCode。
- 如果不重写,这些集合会把对象视为“不同”,即便它们的数据相同。
5. 示例:在集合中如何工作
HashSet:存储唯一对象
Set<Person> people = new HashSet<>();
people.add(new Person("Ivan", 20));
people.add(new Person("Ivan", 20)); // 重复项
System.out.println(people.size()); // 如果 equals/hashCode 实现正确则为 1
如果没有正确的 equals/hashCode,集合会接收两个对象。
HashMap:按键查找
Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Anna", 25);
Person p2 = new Person("Anna", 25);
map.put(p1, "用户 1");
System.out.println(map.get(p2)); // "用户 1",如果 equals/hashCode 实现正确
没有契约,集合会返回 null——对它来说这是“不同”的键。
6. 最佳实践:实现建议
- 在 equals/hashCode 中纳入所有决定对象“标识”的字段。
- 不要在计算 hashCode 时使用会在放入集合后改变的可变字段。
- 让 IDE 生成这些方法——能降低手误风险。
- 在 equals 中先检查 this == o,再检查类,最后比较字段。
- 比较对象字段时使用 Objects.equals。
- 计算哈希码时使用 Objects.hash 或采用乘数 31 的成熟模板。
7. 有用的细节
为什么不能只用 hashCode?
碰撞不可避免:不同对象可能拥有相同的 hashCode。哈希只是用于定位桶的快捷线索;最终的相等判定由 equals 做出。
是否可以不重写 equals 和 hashCode?
只有当你确信对象永远不会按内容被比较,且不会成为集合中的键或唯一元素时才可以。实际中这很少见。
==、equals 与 compareTo 的区别
| 运算符/方法 | 比较什么? | 用途 |
|---|---|---|
|
引用(内存地址) | 检查“是不是同一个对象?” |
|
对象内容 | 基于业务逻辑的相等性 |
|
顺序(小于/等于/大于) | 排序、建立顺序 |
8. 实现 equals 和 hashCode 的常见错误
错误 #1:重写了 equals,却忘了 hashCode。 对象被视为相等,但会落入哈希表的不同桶——查找与删除将会出问题。
错误 #2:在 hashCode 中使用了可变字段。 如果字段在放入集合后发生变化,对象会“丢失”:哈希变了,而桶没有变。
错误 #3:破坏了 equals 的对称性/传递性。 a.equals(b) 为 true,而 b.equals(a) 为 false,或传递性被破坏——集合将表现得不可预测。
错误 #4:在 equals 中未检查类。 比较不同类的对象会导致错误结果或异常。
错误 №5:用 == 比较字符串和对象。 运算符 == 比较的是引用;请使用 equals 来比较内容。
错误 №6:compareTo 与 equals 不一致。 如果 a.compareTo(b) == 0,但 !a.equals(b),那么 TreeSet/TreeMap 可能会把元素视为在顺序上“相等”但在相等性上不同——这会引发“幽灵元素”和重复项。
GO TO FULL VERSION