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

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
我同意 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 ofB
to build the hash code.The
A
s are just containers and their value is fully determined by their content, which is the values of the containedB
s, 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 theB
values (orB
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 containedB
'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 theB
s (if unordered set).Note that if you are using a
java.util.Collection
withinA
, then just fixing theB
s hash code by ignoring the parent reference will automatically give validA
hash codes since theCollection
s have good hash codes by default (ordering or not).在典型模型中,大多数实体都有唯一的 ID。此 ID 在各种用例中都很有用(特别是:数据库检索/查找)。 IIUC,bKey字段应该是这样一个唯一的ID。因此,比较此类实体的常见做法是比较它们的 ID:
您可能会问:“如果两个 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:
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()
orhashCode()
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.您可以有两种
equals
风格:重写Object.equals
和一种更适合递归的风格。递归相等性检查采用 A 或 B——以该类的其他类为准——这是您代表调用递归相等性的对象。如果您代表this.equals
调用它,则传入null
。例如:因此,以下
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
返回递归相等性检查的结果You could have two flavors of
equals
-- the override ofObject.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 ofthis.equals
, you pass innull
. For instance:So, following
A.equals
:A.equals
calls `recursiveEquality(otherA, null)this.b != null
, we end up in the third if-else block, which callsb.recursiveEquality(other.b, this)
B.recursiveEquality
, we hit the first if-else block, which simply asserts that ourA
is the same one that was passed to us (ie, that the circular reference isn't broken)B.recursiveEquality
by checkingaKey
(depending on your invariants, you may want to assert something based on what happened in step 3).B.recursiveEquality
returnsA.recursiveEquality
by checkingbKey
, possibly with similar assertsA.equals
returns the result of the recursive equality check首先,您确定要重写
Equals()
和GetHashCode()
吗?在大多数场景中,您应该可以使用默认的引用相等性。但是,我们假设不是。那么,您想要的适当的相等语义是什么?
例如,假设每个
A
都有一个B
类型的getB
字段,每个B
都有一个getA类型为
字段。令A
的a1
和a2
为两个A
对象,具有相同的字段和相同的getB
(与“相同的内存地址”)b1
。a1
和a2
相等吗?假设b1.getA
与a1
相同(与“相同内存地址”相同),但与a2
不同。您仍然想认为a1
和a2
相等吗?如果不是,请不要覆盖任何内容并使用默认的引用相等性。
如果是,那么这里有一个解决方案:让
A
有一个int GetCoreHashCode()
函数,该函数不依赖于getB
元素,(但依赖于其他领域)。让B
有一个int GetCoreHashCode()
函数,该函数不依赖于 getA 元素(但依赖于其他字段)。现在让A
的int GetHashCode()
函数依赖于this.GetCoreHashCode()
和getB.GetCoreHashCode()
对于B
也是如此,你就完成了。First of all, are you sure you want to override
Equals()
andGetHashCode()
? 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 agetB
field of typeB
and eachB
has agetA
field of typeA
. Leta1
anda2
be twoA
objects, have the same fields and the samegetB
(same as in "same memory address")b1
. Area1
anda2
equal? Supposeb1.getA
is the same asa1
(same as in "same memory address") but not the same asa2
. Do you still want to considera1
anda2
equal?If not, don't override anything and use the default referential equality.
If yes, then here is a solution: Let
A
have aint GetCoreHashCode()
function that does not depend ongetB
element, (but depend on other fields). LetB
have aint GetCoreHashCode()
function that does not depend on getA element, (but depend on other fields). Now letint GetHashCode()
function ofA
depend uponthis.GetCoreHashCode()
andgetB.GetCoreHashCode()
and likewise forB
, and you are done.