Java中循环引用对象的equals和hashCode实现

发布于 2024-12-27 03:51:39 字数 2721 浏览 2 评论 0原文

我定义了两个类,它们都包含对另一个对象的引用。它们看起来与此类似(这是简化的;在我的真实域模型中,类 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 方法中的所有字段,以便对对象进行真正的深度相等比较,但我不知道如何解决这个问题。谢谢!

I have two classes defined such that they both contain references to the other object. They look similar to this (this is simplified; in my real domain model class A contains a list of B and each B has a reference back to parent 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;
    }
}

The hashCode and equals have been generated by Eclipse using both fields of both A and B. The problem is that calling the equals or hashCode method on either object results in a StackOverflowError since they both call the other object's equals and hashCode method. For example the following program will fail with StackOverflowError using the above objects:

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

If there is something inherently wrong with having a domain model defined with circular relationships in this way then please let me know. As far as I can tell though this is a fairly common scenario, correct?

What is best practice for defining hashCode and equals in this case? I want to keep all fields in the equals method so that it is a true deep equality comparison on the object but I don't see how I can with this problem. Thanks!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

盛夏尉蓝 2025-01-03 03:51:39

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

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

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

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

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

I agree with the comment of I82Much that you should avoid having B referencing their parent: it's information duplication, which usually only leads to trouble, but you might need to do so in your case.

Even if you leave the parent reference in B, as far as hash codes are concerned you should completely ignore the parent reference and only use the true inner variables of B to build the hash code.

The As are just containers and their value is fully determined by their content, which is the values of the contained Bs, and so should their hash keys.

If A is an unordered set, you must be very careful that the hash code you are building from the B values (or B hash codes) is not dependent on some ordering. For example, if the hash code is build by adding and multiplying the hash codes of the contained B's in some sequence, you should first order the hash codes by increasing order before computing the result of the sums/multiplications. Similarly, A.equals(o) must not depend on the ordering of the Bs (if unordered set).

Note that if you are using a java.util.Collection within A, then just fixing the Bs hash code by ignoring the parent reference will automatically give valid A hash codes since the Collections have good hash codes by default (ordering or not).

小清晰的声音 2025-01-03 03:51:39

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

In a typical model, most entities have a unique ID. This ID is useful in various use-cases (in particular: Database retreival/lookup). IIUC, the bKey field is supposed to be such a unique ID. Thus, the common practice for comparing such entities is to compare their 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(); }

You may ask: "what happens if two B objects have the same ID but different state (value of their fields are different)". Your code should make sure that such things do not happen. This will be a problem regardless of how you implement equals() or hashCode() because it essentially means that you have two different versions of the same entity in your system and you won't be able to tell which is the correct one.

我早已燃尽 2025-01-03 03:51:39

您可以有两种 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. if this.b != null,我们最终进入第三个 if-else 块,该块调用 b.recursiveEquality(other.b, this)
      1. B.recursiveEquality 中,我们点击了第一个 if-else 块,它只是断言我们的 A 与之前的相同。传递给我们(即循环引用没有被破坏)
      2. 我们通过检查 aKey 来完成 B.recursiveEquality(根据您的不变量,您可能希望根据第 3 步中发生的情况断言某些内容)。 B.recursiveEquality 返回
    2. 我们通过检查 bKey 来完成 A.recursiveEquality,可能使用类似的断言
  2. A.equals 返回递归相等性检查的结果

You could have two flavors of equals -- the override of Object.equals and one that's better suited for recursion. The recursive equality check takes an A or B -- whichever is the other class of this one -- which is the object you're calling the recursive equality on behalf of. If you're calling it on behalf of this.equals, you pass in null. For instance:

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

So, following A.equals:

  1. A.equals calls `recursiveEquality(otherA, null)
    1. if this.b != null, we end up in the third if-else block, which calls b.recursiveEquality(other.b, this)
      1. in B.recursiveEquality, we hit the first if-else block, which simply asserts that our A is the same one that was passed to us (ie, that the circular reference isn't broken)
      2. we finish B.recursiveEquality by checking aKey (depending on your invariants, you may want to assert something based on what happened in step 3). B.recursiveEquality returns
    2. we finish A.recursiveEquality by checking bKey, possibly with similar asserts
  2. A.equals returns the result of the recursive equality check
酒解孤独 2025-01-03 03:51:39

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

但是,我们假设不是。那么,您想要的适当的相等语义是什么?

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

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

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

First of all, are you sure you want to override Equals() and GetHashCode()? In most scenearios you should be fine with the default referential equality.

But, let's suppose not. Than, what is the appropriate equality semantics you want?

For example let's say each A has a getB field of type B and each B has a getA field of type A. Let a1 and a2 be two A objects, have the same fields and the same getB (same as in "same memory address") b1. Are a1 and a2 equal? Suppose b1.getA is the same as a1 (same as in "same memory address") but not the same as a2. Do you still want to consider a1 and a2 equal?

If not, don't override anything and use the default referential equality.

If yes, then here is a solution: Let A have a int GetCoreHashCode() function that does not depend on getB element, (but depend on other fields). Let B have a int GetCoreHashCode() function that does not depend on getA element, (but depend on other fields). Now let int GetHashCode() function of A depend upon this.GetCoreHashCode() and getB.GetCoreHashCode() and likewise for B, and you are done.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文