可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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!
回答1:
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 A
s are just containers and their value is fully determined by their content, which is the values of the contained B
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 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 B
s (if unordered set).
Note that if you are using a java.util.Collection
within A
, then just fixing the B
s hash code by ignoring the parent reference will automatically give valid A
hash codes since the Collection
s have good hash codes by default (ordering or not).
回答2:
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.
回答3:
Here are a few libraries for this purpose :
https://code.google.com/p/deep-equals/
http://sourceforge.net/projects/unitils/
https://github.com/SQiShER/java-object-diff
回答4:
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
:
A.equals
calls `recursiveEquality(otherA, null)
- if
this.b != null
, we end up in the third if-else block, which calls b.recursiveEquality(other.b, this)
- 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)
- 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
- we finish
A.recursiveEquality
by checking bKey
, possibly with similar asserts
A.equals
returns the result of the recursive equality check
回答5:
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.