[英]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的两个字段生成了hashCode
和equals
。问题是,在两个对象中的任何一个上调用equals
或hashCode
方法都会导致StackOverflowError
因为它们都在调用另一个对象的equals
和hashCode
方法。 例如,以下程序使用上述对象将失败并出现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));
}
如果用这种方式用循环关系定义域模型存在内在的错误,请告诉我。 据我所知,虽然这是相当普遍的情况,对吗?
在这种情况下,定义hashCode
和equals
最佳实践是什么? 我想将所有字段保留在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
:
A.equals
调用`recursiveEquality(otherA,null)
this.b != null
,则结束于第三个if-else块,该块调用b.recursiveEquality(other.b, this)
B.recursiveEquality
,我们到达了第一个 if-else块,该块简单地断言我们的A
与传递给我们的那个相同(即,循环引用未中断) aKey
完成B.recursiveEquality
(取决于您的不变量,您可能要根据步骤3中发生的事情来断言某些内容)。 B.recursiveEquality
回报 bKey
完成A.recursiveEquality
,可能具有类似的断言 A.equals
返回递归相等检查的结果 首先,您确定要覆盖Equals()
和GetHashCode()
吗? 在大多数场景中,您都可以使用默认的参照相等性。
但是,让我们假设不是。 然后,您想要什么合适的相等语义?
例如,假设每个A
具有类型B
的getB
字段,每个B
具有类型A
的getA
字段。 假设a1
和a2
是两个A
对象,具有相同的字段和相同的getB
(与“相同的内存地址”相同) b1
。 a1
和a2
相等吗? 假设b1.getA
与a1
相同(与“相同的内存地址”相同),但与a2
。 您是否仍要考虑a1
和a2
相等?
如果没有,则不要覆盖任何内容,并使用默认的引用相等性。
如果是,那么这里是一个解决方案:让A
具有一个不依赖于getB
元素(但依赖于其他字段)的int GetCoreHashCode()
函数。 令B
具有一个不依赖于getA元素(但依赖于其他字段)的int GetCoreHashCode()
函数。 现在,让我们int GetHashCode()
的函数A
取决于this.GetCoreHashCode()
和getB.GetCoreHashCode()
同样为B
,和你做。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.