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

时间:2012-01-14 15:52:28

标签: java equals hashcode

我定义了两个类,它们都包含对另一个对象的引用。它们看起来与此类似(这是简化的;在我的真实域模型中,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的两个字段生成hashCodeequals。问题是在任一对象上调用equalshashCode方法结果为StackOverflowError,因为它们都调用另一个对象的equalshashCode方法。例如,以下程序将使用上述对象StackOverflowError失败:

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

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

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

4 个答案:

答案 0 :(得分:5)

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

即使您将父引用保留在B中,就哈希代码而言,您应该完全忽略父引用,并且只使用{{1}的 true 内部变量构建哈希码。

B只是容器,它们的值完全取决于它们的内容,即包含的A s的值,它们的哈希键也是如此。

如果B是无序集,则必须非常小心,您从A值(或B哈希码)构建的哈希码不依赖于某些排序。例如,如果通过在某个序列中添加和乘以包含的B的哈希码来构建哈希码,则应首先通过在计算求和/乘法的结果之前按顺序对哈希码进行排序。同样,B不得依赖于A.equals(o)的排序(如果是无序集)。

请注意,如果您在B中使用java.util.Collection,那么只需通过忽略父引用来修复A哈希码,就会自动提供有效的B哈希码因为A默认具有良好的哈希码(订购与否)。

答案 1 :(得分:4)

在典型模型中,大多数实体都具有唯一ID。此ID在各种用例中非常有用(特别是:数据库retreival / lookup)。在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(),这都是一个问题,因为它实际上意味着您的系统中有相同实体的两个不同版本,而您将无法分辨哪个是纠正一个。

答案 2 :(得分:0)

你可以有两种equals - 覆盖Object.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.equals

  1. A.equals调用`recursiveEquality(otherA,null)
    1. 如果this.b != null,我们会在第三个if-else块中结束,该块调用b.recursiveEquality(other.b, this)
      1. B.recursiveEquality中,我们点击第一个 if-else块,它只是断言我们的A与传递给我们的B.recursiveEquality相同(即,循环引用未被破坏)
      2. 我们通过检查aKey完成B.recursiveEquality(根据您的不变量,您可能希望根据步骤3中发生的事情断言)。 A.recursiveEquality返回
    2. 我们通过检查bKey完成了A.equals,可能还有类似的断言
  2. {{1}}返回递归相等检查的结果

答案 3 :(得分:-2)

首先,您确定要覆盖Equals()GetHashCode()吗?在大多数场景中,您应该没有使用默认的引用相等性。

但是,我们假设没有。比,你想要的适当的等式语义是什么?

例如,假设每个A都有一个getB类型的B字段,而每个B都有一个类型为getA的{​​{1}}字段。让Aa1为两个a2个对象,具有相同的字段和相同的A(与“相同的内存地址”相同)getBb1a1相等吗?假设a2b1.getA相同(与“相同的内存地址”相同),但与a1不同。您是否仍要考虑a2a1相等?

如果没有,请不要覆盖任何内容并使用默认的引用相等性。

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