繁体   English   中英

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

[英]Implementing equals and hashCode for objects with circular references in Java

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

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

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

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

如果A是无序集合,则必须非常小心,以确保从B值(或B哈希码)构建的哈希码不依赖于某些排序。 例如,如果哈希码是通过将包含的B的哈希码按一定顺序相乘并相乘而构建的,则在计算求和/乘法结果之前,应首先按递增顺序对哈希码进行排序。 同样, A.equals(o)不得依赖于B的排序(如果是无序集合)。

请注意,如果您在A内使用java.util.Collection ,则仅通过忽略父引用来固定B的哈希码将自动给出有效的A哈希码,因为Collection默认情况下具有良好的哈希码(有序或无序) 。

在典型模型中,大多数实体具有唯一的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 -重写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与传递给我们的那个相同(即,循环引用未中断)
      2. 我们通过检查aKey完成B.recursiveEquality (取决于您的不变量,您可能要根据步骤3中发生的事情来断言某些内容)。 B.recursiveEquality回报
    2. 我们通过检查bKey完成A.recursiveEquality ,可能具有类似的断言
  2. A.equals返回递归相等检查的结果

首先,您确定要覆盖Equals()GetHashCode()吗? 在大多数场景中,您都可以使用默认的参照相等性。

但是,让我们假设不是。 然后,您想要什么合适的相等语义?

例如,假设每个A具有类型BgetB字段,每个B具有类型AgetA字段。 假设a1a2是两个A对象,具有相同的字段和相同的getB (与“相同的内存地址”相同) b1 a1a2相等吗? 假设b1.getAa1相同(与“相同的内存地址”相同),但与a2 您是否仍要考虑a1a2相等?

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

如果是,那么这里是一个解决方案:让A具有一个不依赖于getB元素(但依赖于其他字段)的int GetCoreHashCode()函数。 B具有一个不依赖于getA元素(但依赖于其他字段)的int GetCoreHashCode()函数。 现在,让我们int GetHashCode()的函数A取决于this.GetCoreHashCode()getB.GetCoreHashCode()同样为B ,和你做。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM