
当我们在java中操作集合或使用流api来去重时,常常会遇到一个问题:即使我们认为两个自定义对象在逻辑上是相同的(例如,它们的所有属性值都相等),但它们却被集合或流api视为不同的对象。这通常发生在尝试使用 hashset 存储自定义对象或对自定义对象列表使用 stream().distinct() 方法时。
考虑一个 PointType 类,它有两个 double 类型的坐标 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() 方法
}在上述代码中,尽管我们尝试重写 equals 方法来比较 x 和 y 属性,但将其放入 HashSet 或通过 distinct() 处理时,仍然可能无法正确识别具有相同 x 和 y 值的不同 PointType 实例为唯一对象。这是因为Java的集合框架和流API在判断对象相等性时,不仅仅依赖于 equals() 方法,对于哈希相关的操作(如 HashSet 和 distinct()),hashCode() 方法也扮演着至关重要的角色。
equals() 方法定义了两个对象在逻辑上是否相等。Java规范对 equals() 方法有严格的契约要求:
针对 PointType 类,一个更健壮的 equals() 实现应该遵循以下步骤:
立即学习“Java免费学习笔记(深入)”;
@Override
public boolean equals(Object o) {
// 1. 自反性优化:如果对象是自身,直接返回 true
if (this == o) return true;
// 2. 非空性检查和类型检查:
// o == null 检查:如果传入对象为null,则不相等
// getClass() != o.getClass() 检查:确保比较的是相同运行时类型的对象
// (注意:有时会使用 instanceof,但 getClass() 更严格,推荐用于类层次结构扁平的情况)
if (o == null || getClass() != o.getClass()) return false;
// 3. 类型转换:将 Object 转换为当前类型
PointType pointType = (PointType) o;
// 4. 属性比较:比较所有用于定义对象逻辑相等性的关键属性
// 对于 double 类型,直接使用 == 可能会因浮点数精度问题导致不准确。
// 推荐使用 Double.compare() 来进行安全的浮点数比较。
return Double.compare(pointType.x, x) == 0 &&
Double.compare(pointType.y, y) == 0;
}初始 equals 方法的不足在于:
hashCode() 方法返回对象的哈希码,它是一个整数值。这个方法主要用于哈希表(如 HashMap、HashSet)中,以提高查找效率。hashCode() 方法也有其严格的契约:
equals() 和 hashCode() 必须同时正确实现,否则哈希集合(如 HashSet)和依赖哈希码的流操作(如 distinct())将无法正常工作。如果两个对象 equals 返回 true 但 hashCode 不同,那么它们在哈希表中可能被存储在不同的位置,导致无法被正确识别为同一个对象。
针对 PointType 类,一个简单的 hashCode() 实现可以使用 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() 自动为所有关键属性生成哈希码
return Objects.hash(x, y);
}
}Objects.hash() 是一个非常方便的工具,它会为传入的所有参数生成一个合理的哈希码,并自动处理基本类型和 null 值。
一旦 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 PointTypeUniquenessTest {
@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); // 应该被视为重复
setB.add(p1);
setB.add(p2); // 应该被视为重复
setB.add(p3);
setB.add(p4); // 应该被视为重复
// 使用 ArrayList 和 Stream.distinct() 验证唯一性
listA.add(p1);
listA.add(p2);
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());
// 验证 equals 方法是否正确
assertTrue(p1.equals(p2)); // 应该通过
assertTrue(p3.equals(p4)); // 应该通过
// 验证 HashSet 的唯一性
assertTrue(setA.size() == 1); // 应该通过,因为 p1 和 p2 逻辑相等
assertTrue(setB.size() == 2); // 应该通过,因为 (p1,p2) 和 (p3,p4) 是两组逻辑相等对象
// 验证 Stream.distinct() 的唯一性
assertTrue(listA.size() == 1); // 应该通过
assertTrue(listB.size() == 2); // 应该通过
}
}在 PointType 正确实现 equals() 和 hashCode() 后,上述所有的 assertTrue 断言都将成功通过。这证明了 HashSet 和 Stream.distinct() 现在能够根据我们自定义的逻辑相等性来正确地识别和处理对象的唯一性。
在Java中处理自定义对象的唯一性是常见的需求,而 equals() 和 hashCode() 方法是实现这一目标的核心。理解它们的契约,并按照最佳实践正确实现这两个方法,不仅能确保 HashSet 和 Stream.distinct() 等集合和流操作的正确性,还能提高程序的健壮性和可维护性。正确地管理自定义对象的相等性,是构建高效且可靠Java应用程序的关键一步。
以上就是Java自定义对象唯一性:深入理解 equals() 与 hashCode()的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号