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

2022-09-01 05:30:20

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

public class A {

    public B b;
    public String bKey;

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((b == null) ? 0 : b.hashCode());
        result = prime * result + ((bKey == null) ? 0 : bKey.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (!(obj instanceof A))
            return false;
        A other = (A) obj;
        if (b == null) {
            if (other.b != null)
                return false;
        } else if (!b.equals(other.b))
            return false;
        if (bKey == null) {
            if (other.bKey != null)
                return false;
        } else if (!bKey.equals(other.bKey))
            return false;
        return true;
    }
}

public class B {

    public A a;
    public String aKey;

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((a == null) ? 0 : a.hashCode());
        result = prime * result + ((aKey == null) ? 0 : aKey.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (!(obj instanceof B))
            return false;
        B other = (B) obj;
        if (a == null) {
            if (other.a != null)
                return false;
        } else if (!a.equals(other.a))
            return false;
        if (aKey == null) {
            if (other.aKey != null)
                return false;
        } else if (!aKey.equals(other.aKey))
            return false;
        return true;
    }
}

和 是由 Eclipse 使用 A 和 B 的两个字段生成的。问题在于,在任一对象上调用 or 方法会导致 ,因为它们都调用另一个对象和方法。例如,以下程序在使用上述对象时将失败:hashCodeequalsequalshashCodeStackOverflowErrorequalshashCodeStackOverflowError

    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));
    }

如果以这种方式使用循环关系定义域模型存在固有的错误,那么请告诉我。据我所知,这是一个相当普遍的情况,对吧?

在这种情况下,定义和的最佳实践是什么?我想将所有字段保留在方法中,以便它是对象上真正深入的相等比较,但我不明白如何解决这个问题。谢谢!hashCodeequalsequals


答案 1

我同意I82Much的评论,即您应该避免让B引用其父级:这是信息重复,通常只会导致麻烦,但您可能需要这样做。

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

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

如果 是无序集,则必须非常小心,从值(或哈希代码)构建的哈希代码不依赖于某些排序。例如,如果哈希代码是通过按某种顺序将所包含的 的哈希代码相加和相乘来构建的,则在计算总和/乘法的结果之前,应首先按递增的顺序对哈希代码进行排序。同样,不得依赖于 s 的顺序(如果是无序集)。ABBBA.equals(o)B

请注意,如果您使用的是 within ,则只需通过忽略父引用来修复 s 哈希代码将自动给出有效的哈希代码,因为默认情况下 s 具有良好的哈希代码(排序与否)。java.util.CollectionABACollection


答案 2

在典型模型中,大多数实体都具有唯一的 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()


推荐