
在Java中,判断两个对象是否“相等”有两种主要方式:
当我们需要在集合(如HashSet)或流操作(如Stream.distinct())中基于对象的内容来判断唯一性时,就必须重写equals()方法。然而,仅仅重写equals()是不够的,还需要同时重写hashCode()方法,以确保这些机制能够正常工作。
考虑以下PointType类,它代表一个二维点,并尝试通过x和y坐标来定义其唯一性:
public class PointType {
private double x;
private double y;
public PointType(double x, double y) {
this.x = x;
this.y = y;
}
// 初始尝试的equals方法
@Override
public boolean equals(Object other) {
if (other instanceof PointType && this.x == ((PointType) other).x && this.y == ((PointType) other).y) {
return true;
}
return false;
}
// 缺少 hashCode 方法
}在使用上述PointType类进行去重操作时,例如:
立即学习“Java免费学习笔记(深入)”;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class UniquePointTest {
@Test
public void testUniqueness() {
Set<PointType> setA = new HashSet<>();
Set<PointType> setB = new HashSet<>();
List<PointType> listA = new ArrayList<>();
List<PointType> listB = new ArrayList<>();
PointType p1 = new PointType(1.0, 2.0);
PointType p2 = new PointType(1.0, 2.0); // 逻辑上与p1相等
PointType p3 = new PointType(2.0, 2.0);
PointType p4 = new PointType(2.0, 2.0); // 逻辑上与p3相等
// 尝试使用HashSet去重
setA.add(p1);
setA.add(p2);
setB.add(p1);
setB.add(p2);
setB.add(p3);
setB.add(p4);
// 尝试使用Stream.distinct()去重
listA.add(p1);
listA.add(p2);
listA = listA.stream().distinct().collect(Collectors.toList());
listB.add(p1);
listB.add(p2);
listB.add(p3);
listB.add(p4);
listB = listB.stream().distinct().collect(Collectors.toList());
assertTrue(p1.equals(p2)); // 通过
assertTrue(p3.equals(p4)); // 通过
assertTrue(setA.size() == 1); // 通过,因为p1和p2是同一组逻辑相等对象
assertTrue(setB.size() == 2); // 失败!预期是2,实际可能是4
assertTrue(listA.size() == 1); // 通过
assertTrue(listB.size() == 2); // 失败!预期是2,实际可能是4
}
}上述测试中,setB.size()和listB.size()的断言会失败。这是因为HashSet和Stream.distinct()(其内部实现通常依赖于哈希表)在判断对象唯一性时,不仅会调用equals()方法,还会依赖hashCode()方法。如果hashCode()没有被正确重写,或者与equals()不一致,它们将无法正确识别逻辑上相等的对象。
Java规范对equals()和hashCode()方法有明确的契约要求:
最关键的是equals()和hashCode()之间的一致性契约:
原equals方法存在一些不足,尤其是直接使用==比较double类型可能导致精度问题,并且缺乏对null和类型不匹配的严格检查。一个更健壮的equals实现应遵循以下模式:
public class PointType {
private double x;
private double y;
public PointType(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
// 1. 引用相等性检查:如果两者是同一个对象,直接返回true
if (this == o) return true;
// 2. null检查和类型检查:如果o为null或类型不匹配,返回false
if (o == null || getClass() != o.getClass()) return false;
// 3. 类型转换
PointType pointType = (PointType) o;
// 4. 属性比较:对于double类型,使用Double.compare()进行安全的比较
return Double.compare(pointType.x, x) == 0 &&
Double.compare(pointType.y, y) == 0;
}
// 暂时省略 hashCode
}说明:
hashCode()的实现必须与equals()保持一致。如果两个PointType对象根据其x和y坐标是相等的,那么它们的hashCode也必须相同。Java 7引入的Objects.hash()方法提供了一种简洁且安全的方式来生成哈希码:
import java.util.Objects; // 导入Objects类
public class PointType {
private double x;
private double y;
public PointType(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PointType pointType = (PointType) o;
return Double.compare(pointType.x, x) == 0 &&
Double.compare(pointType.y, y) == 0;
}
@Override
public int hashCode() {
// 使用Objects.hash()为所有参与equals比较的字段生成哈希码
return Objects.hash(x, y);
}
}说明:
现在,使用修正后的PointType类重新运行之前的测试:
// ... (PointType类已如上所示修正)
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class UniquePointTest {
@Test
public void testUniqueness() {
Set<PointType> setA = new HashSet<>();
Set<PointType> setB = new HashSet<>();
List<PointType> listA = new ArrayList<>();
List<PointType> listB = new ArrayList<>();
PointType p1 = new PointType(1.0, 2.0);
PointType p2 = new PointType(1.0, 2.0);
PointType p3 = new PointType(2.0, 2.0);
PointType p4 = new PointType(2.0, 2.0);
setA.add(p1);
setA.add(p2); // p2与p1逻辑相等,HashSet会将其视为重复元素
setB.add(p1);
setB.add(p2); // p2与p1逻辑相等
setB.add(p3);
setB.add(p4); // p4与p3逻辑相等
listA.add(p1);
listA.add(p2);
listA = listA.stream().distinct().collect(Collectors.toList());
listB.add(p1);
listB.add(p2);
listB.add(p3);
listB.add(p4);
listB = listB.stream().distinct().collect(Collectors.toList());
assertTrue(p1.equals(p2)); // 通过
assertTrue(p3.equals(p4)); // 通过
assertTrue(setA.size() == 1); // 通过,setA现在只包含一个元素 (p1)
assertTrue(setB.size() == 2); // 通过,setB现在包含两个元素 (p1, p3)
assertTrue(listA.size() == 1); // 通过
assertTrue(listB.size() == 2); // 通过
}
}现在所有的断言都将通过。这表明通过正确实现equals()和hashCode()方法,HashSet和Stream.distinct()能够准确地识别基于对象内容定义的唯一性。
在Java中处理自定义对象的唯一性是一个常见而重要的任务。其关键在于正确地重写equals()和hashCode()方法,并严格遵循它们之间的契约。通过规范地实现这两个方法,我们可以确保HashSet、HashMap以及Stream.distinct()等集合和流操作能够准确地识别和处理逻辑上相等的对象,从而构建出健壮且行为符合预期的Java应用程序。
以上就是Java自定义对象唯一性:深入理解与正确实现equals和hashCode的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号