
在Java编程中,我们经常需要处理包含唯一元素的集合。对于基本数据类型或Java内置的包装类(如String, Integer等),其唯一性判断通常是直观的。然而,当我们创建自定义类并希望根据其特定属性(而非内存地址)来判断对象是否唯一时,问题便会浮现。例如,两个PointType对象,即使它们是不同的实例,但如果它们的x和y坐标值相同,我们可能希望它们被认为是“相等”的,并且在集合中只保留一个。
Java集合框架中的HashSet和Stream API的distinct()方法是实现唯一性筛选的常用工具。然而,它们的工作机制依赖于对象内部的两个核心方法:equals()和hashCode()。如果这两个方法没有被正确地重写,即使对象的业务属性相同,它们也可能被视为不同的对象。
考虑一个简单的PointType类,它包含x和y两个double类型的坐标:
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) {
// 错误的实现:未进行类型检查和null检查,double比较不严谨
if (other instanceof PointType && this.x == ((PointType)other).x && this.y == ((PointType)other).y) {
return true;
}
return false;
}
// 缺少hashCode方法
}在上述代码中,equals方法虽然尝试比较x和y,但存在几处问题:
立即学习“Java免费学习笔记(深入)”;
当使用这样的PointType对象放入HashSet或通过Stream.distinct()进行筛选时,会观察到不符合预期的结果:
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);
setA.add(p1);
setA.add(p2);
// 预期:setA.size() == 1 (如果p1和p2被认为是相等的)
// 实际:setA.size() == 2 (因为缺少hashCode,p1和p2被认为是不同的对象)
setB.add(p1);
setB.add(p2);
setB.add(p3);
setB.add(p4);
// 预期:setB.size() == 2 (p1/p2一组,p3/p4一组)
// 实际:setB.size() == 4
// 使用ArrayList和Stream.distinct()
listA.add(p1);
listA.add(p2);
listA.add(p1);
listA.add(p2);
listA = listA.stream().distinct().collect(Collectors.toList());
// 预期:listA.size() == 1
// 实际:listA.size() == 2
listB.add(p1);
listB.add(p2);
listB.add(p3);
listB.add(p4);
listB = listB.stream().distinct().collect(Collectors.toList());
// 预期:listB.size() == 2
// 实际:listB.size() == 4
// 验证equals方法本身
assertTrue(p1.equals(p2)); // 这个测试通过,说明equals方法在直接比较时有效
assertTrue(p3.equals(p4)); // 这个测试通过
// 验证集合和流的唯一性(在缺少hashCode的情况下,这些测试会失败)
// assertTrue(setA.size() == 1); // 失败
// assertTrue(setB.size() == 2); // 失败
// assertTrue(listA.size() == 1); // 失败
// assertTrue(listB.size() == 2); // 失败
}
}尽管p1.equals(p2)返回true,但HashSet和Stream.distinct()仍然将它们视为不同的对象。这是因为这些集合和流操作在内部依赖于hashCode()方法来快速定位和比较对象。
Java规范明确指出,如果重写了equals()方法,就必须重写hashCode()方法,并且要遵循以下约定:
一个健壮的equals()方法实现应遵循以下步骤:
import java.util.Objects; // 用于简化hashCode的生成
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 o) {
// 1. 同一性检查
if (this == o) return true;
// 2. null值检查 和 类型检查
// 使用getClass() != o.getClass() 进行严格的类型匹配
if (o == null || getClass() != o.getClass()) return false;
// 3. 类型转换
PointType pointType = (PointType) o;
// 4. 属性比较,使用Double.compare()安全比较double类型
return Double.compare(pointType.x, x) == 0 &&
Double.compare(pointType.y, y) == 0;
}
// 缺少hashCode方法,下一步将补充
}hashCode()方法应基于与equals()方法中使用的相同属性来生成哈希码。Java 7 引入的Objects.hash()方法极大地简化了hashCode()的实现,它能够为多个字段生成一个高质量的哈希码。
import java.util.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;
}
// 正确的hashCode方法实现
@Override
public int hashCode() {
// 使用Objects.hash()简化生成,将所有参与equals比较的字段作为参数
return Objects.hash(x, y);
}
}在PointType类中正确实现equals()和hashCode()之后,之前的测试用例将全部通过:
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 testUniquenessAfterFix() {
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);
setA.add(p1);
setA.add(p2);
assertTrue(setA.size() == 1); // 现在通过!p1和p2被视为同一个对象
setB.add(p1);
setB.add(p2);
setB.add(p3);
setB.add(p4);
assertTrue(setB.size() == 2); // 现在通过!p1/p2一组,p3/p4一组
listA.add(p1);
listA.add(p2);
listA.add(p1);
listA.add(p2);
listA = listA.stream().distinct().collect(Collectors.toList());
assertTrue(listA.size() == 1); // 现在通过!
listB.add(p1);
listB.add(p2);
listB.add(p3);
listB.add(p4);
listB = listB.stream().distinct().collect(Collectors.toList());
assertTrue(listB.size() == 2); // 现在通过!
// 确保equals方法本身仍然正确
assertTrue(p1.equals(p2));
assertTrue(p3.equals(p4));
}
}在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号