【问题标题】:Implementing equals and hashCode for objects with circular references in Java在Java中为具有循环引用的对象实现equals和hashCode
【发布时间】:2012-02-10 09:47:59
【问题描述】:

我定义了两个类,它们都包含对另一个对象的引用。它们看起来与此类似(这是简化的;在我的真实域模型中,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;
    }
}

hashCodeequals 是由 Eclipse 使用 A 和 B 的两个字段生成的。问题是在任一对象上调用 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 方法中,以便对对象进行真正的深度相等比较,但我不知道如何解决这个问题。谢谢!

【问题讨论】:

  • 为什么需要子项对父项有引用?在序列化方面,这会让你的生活变得非常复杂
  • 我正在使用遗留域模型。我的序列化基本上忽略了孩子的父关系,然后修复了反序列化的关系。在域模型中循环引用不是很常见吗?底层数据库关系是 OneToMany,它被 JBoss 工具逆向工程为带有循环引用的 JPA 对象。

标签: java equals hashcode


【解决方案1】:

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

即使您将父引用留在B 中,就哈希码而言,您应该完全忽略父引用并且只使用Btrue 内部变量来构建哈希码。

As 只是容器,它们的值完全取决于它们的内容,即包含的Bs 的值,它们的哈希键也应该如此。

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

请注意,如果您在A 中使用java.util.Collection,那么只需通过忽略父引用来修复Bs 哈希码将自动给出有效的A 哈希码,因为Collections 具有良好的默认情况下的哈希码(排序与否)。

【讨论】:

  • 我不太明白这是怎么像你说的那样重复信息。一个人可能没有严格处理 A,因此您可以推断 B 的父级是 A,因为它是从 A 引用的。您可能只有 B 的句柄,并且需要了解有关它引用的 A 的一些信息,对吗?如果 B 没有对 A 的引用,则该信息将不存在。
  • 这取决于你在做什么;也许你真的需要找到 B 的父母。但是你通常应该设计你的代码,这样你就可以避免这种情况。如果无法避免,则必须非常小心,以免陷入不一致的状态。
  • 我会说双向链表有数据重复,在某些情况下它对提高效率很有用。但是,您永远不会进入不一致的状态,因为 API 总是同时修改所有权的双方。
【解决方案2】:

你可以有两种风格的equals——覆盖Object.equals,一种更适合递归。递归相等性检查采用 A 或 B - 以该类的 other 类为准 - 这是您代表调用递归相等性的对象。如果您代表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 与传递给我们的相同(即循环引用不是' t坏了)
      2. 我们通过检查 aKey 来完成 B.recursiveEquality(根据您的不变量,您可能希望根据步骤 3 中发生的情况断言某些内容)。 B.recursiveEquality返回
    2. 我们通过检查 bKey 来完成 A.recursiveEquality,可能使用类似的断言
  2. A.equals 返回递归相等检查的结果

【讨论】:

  • 这可能会奏效,但是,我正在寻找域模型最佳实践与所展示的递归问题的特定解决方案。
【解决方案3】:

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

但是,我们假设不是。比,你想要什么合适的相等语义?

例如,假设每个A 都有一个getB 类型为B 的字段,每个B 都有一个getA 类型为A 的字段。设a1a2 为两个A 对象,具有相同的字段和相同的getB(与“相同内存地址”中相同)b1a1a2 是否相等?假设b1.getAa1 相同(与“相同内存地址”相同)但与a2 不同。你还想考虑a1a2 相等吗?

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

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

【讨论】:

  • 这可能会奏效,但是,我正在寻找域模型最佳实践与所展示的递归问题的特定解决方案。
【解决方案4】:

在典型模型中,大多数实体都有唯一的 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(),这都会是一个问题,因为它本质上意味着您的系统中有同一实体的两个不同版本,您将无法分辨哪个是正确的。

【讨论】:

  • 这对我来说很有意义。然而,就我而言,我想要一个全面的 equals 方法供我的自定义序列化程序 junit 测试使用。在我的单元测试中,我想确保对象以正确的方式序列化和反序列化,因此我测试了每个字段。在实践中,我的领域模型可能会很好地让我的 equals 方法测试对象 ID。
  • 这确实有助于解决我的最佳实践问题。
猜你喜欢
  • 2019-06-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-07-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多