在Java中为具有循环引用的对象实现equals和hashCode

在Java中为具有循环引用的对象实现equals和hashCode,java,equals,hashcode,Java,Equals,Hashcode,我定义了两个类,它们都包含对另一个对象的引用。它们看起来类似于此(这是简化的;在我的真实域模型中,类A包含B的列表,每个B都有一个对父A的引用): Eclipse使用A和B两个字段生成了hashCode和equals。问题是对任一对象调用equals或hashCode方法都会导致StackOverflowError,因为它们都调用另一个对象的equals和hashCode方法。例如,使用上述对象,以下程序将因StackOverflowerError而失败: public static v

我定义了两个类,它们都包含对另一个对象的引用。它们看起来类似于此(这是简化的;在我的真实域模型中,类A包含B的列表,每个B都有一个对父A的引用):

Eclipse使用A和B两个字段生成了
hashCode
equals
。问题是对任一对象调用
equals
hashCode
方法都会导致
StackOverflowError
,因为它们都调用另一个对象的
equals
hashCode
方法。例如,使用上述对象,以下程序将因
StackOverflowerError
而失败:

    public static void main(String[] args) {

        A a = new A();
        B b = new B();
        a.b = b;
        b.a = a;

        A a1 = new A();
        B b1 = new B();
        a1.b = b1;
        b1.a = a1;

        System.out.println(a.equals(a1));
    }
如果以这种方式定义具有循环关系的域模型存在固有的问题,那么请让我知道。据我所知,这是一种相当常见的情况,对吗


在这种情况下,定义
hashCode
等于
的最佳实践是什么?我想保留
equals
方法中的所有字段,以便对对象进行真正的深度相等比较,但我不知道如何解决这个问题。谢谢

在典型的模型中,大多数实体都有一个唯一的ID。该ID在各种用例中都很有用(特别是:数据库检索/查找)。IIUC中,bKey字段应该是这样一个唯一的ID。因此,比较此类实体的常见做法是比较其ID:

@Override
public boolean equals(Object obj) {
    if (obj == null)
        return false;
    if (!getClass().equals(obj.getClass()))
        return false;
    return this.bKey.equals(((B) obj).bKey);
}


@Override
public int hashCode() { return bKey.hashCode(); }

您可能会问:“如果两个B对象具有相同的ID但状态不同(它们的字段值不同),会发生什么情况?”。您的代码应该确保这些事情不会发生。无论如何实现
equals()
hashCode(),这都是一个问题
因为这本质上意味着你的系统中有同一实体的两个不同版本,你无法分辨哪个版本是正确的。

我同意I82的评论,你应该避免B引用它们的父版本:这是信息重复,通常只会导致麻烦,但在你的情况下,你可能需要这样做

即使您将父引用保留在
B
中,就哈希代码而言,您也应该完全忽略父引用,只使用
B
的真正内部变量来构建哈希代码

A
s只是容器,它们的值完全由它们的内容决定,即所包含的
B
s的值,它们的哈希键也应该如此

如果
A
是无序集,则必须非常小心,从
B
值(或
B
哈希代码)生成的哈希代码不依赖于某些顺序。例如,如果哈希代码是通过将包含的
B
的哈希代码按一定顺序相加和相乘而生成的,则在计算求和/相乘的结果之前,应首先按递增顺序对哈希代码排序。类似地,
A.equals(o)
不得依赖于
B
s的顺序(如果为无序集)


请注意,如果您在
a
中使用
java.util.Collection
,那么只要通过忽略父引用来修复
B
s哈希代码,就会自动给出有效的
a
哈希代码,因为
集合在默认情况下(排序与否)具有良好的哈希代码,是否确实要覆盖
Equals()
GetHashCode()
?在大多数场景中,您应该可以使用默认的引用等式

但是,让我们假设不是。然后,您需要什么样的适当的相等语义

例如,假设每个
A
都有一个
getB
类型的字段
B
,每个
B
都有一个
getA
类型的字段
A
。让
a1
a2
成为两个
A
对象,具有相同的字段和相同的
getB
(与“相同内存地址”中相同)
b1
a1
a2
是否相等?假设
b1.getA
a1
相同(与“相同内存地址”中的相同),但与
a2
不同。你还想考虑<代码> A1 < /代码>和<代码> A2<代码>相等>?p> 如果不是,则不要重写任何内容,并使用默认的引用等式


如果是,那么这里有一个解决方案:让
a
有一个
int-GetCoreHashCode()
函数,该函数不依赖于
getB
元素(但依赖于其他字段)。让
B
具有一个不依赖于getA元素(但依赖于其他字段)的
int GetCoreHashCode()
函数。现在让
A
int GetHashCode()
函数依赖于
this.GetCoreHashCode()
getB.GetCoreHashCode()
,同样对于
B
,完成了。

您可以有两种类型的
equals
——重写
对象.equals
,还有一种更适合递归。递归相等性检查采用A或B(以本类的另一个类为准),这是您正在代表其调用递归相等性的对象。如果您代表
this.equals调用它,则传入
null
。例如:

A {
    ...
    @Override
    public boolean equals(Object obj) {
        // check for this, null, instanceof...
        A other = (A) obj;
        return recursiveEquality(other, null);
    }

    // package-private, optionally-recursive equality
    boolean recursiveEquality(A other, B onBehalfOf) {
        if (onBehalfOf != null) {
            assert b != onBehalfOf;
            // we got here from within a B.equals(..) call, so we just need
            // to check that our B is the same as the one that called us.
        }
        // At this point, we got called from A.equals(Object). So,
        // need to recurse.
        else if (b == null) {
            if (other.b != null)
                return false;
        }
        // B has a similar structure. Call its recursive-aware equality,
        // passing in this for the onBehalfOf
        else if (!b.recursiveEquality(other.b, this))
            return false;

        // check bkey and return
    }
}
因此,下面的A等于

  • A.equals
    调用`recursiveEquality(otherA,null)
  • 如果
    this.b!=null
    ,我们在第三个if-else块中结束,它调用
    b.recursiveEquality(other.b,this)
  • B.recursiveEquality
    中,我们点击了第一个if-else块,它简单地断言我们的
    A
    与传递给我们的是同一个块(即循环引用没有被破坏)
  • 我们通过检查
    aKey
    来完成
    B.recursiveEquality
    (根据您的不变量,您可能希望asse
    A {
        ...
        @Override
        public boolean equals(Object obj) {
            // check for this, null, instanceof...
            A other = (A) obj;
            return recursiveEquality(other, null);
        }
    
        // package-private, optionally-recursive equality
        boolean recursiveEquality(A other, B onBehalfOf) {
            if (onBehalfOf != null) {
                assert b != onBehalfOf;
                // we got here from within a B.equals(..) call, so we just need
                // to check that our B is the same as the one that called us.
            }
            // At this point, we got called from A.equals(Object). So,
            // need to recurse.
            else if (b == null) {
                if (other.b != null)
                    return false;
            }
            // B has a similar structure. Call its recursive-aware equality,
            // passing in this for the onBehalfOf
            else if (!b.recursiveEquality(other.b, this))
                return false;
    
            // check bkey and return
        }
    }