Equals (Settled Once And For All)
By Adrian Sutton
Andraes Schaefer finally comes up with a solution to the great equals debate. It turns out that in fact it is possible to implement equals in such a way that it works well with subclasses that add additional constraints to equals.
Andraes’ solution is still not great though because of the restrictions he mentions:
- All sub class must overwrite and adjust the equals() method otherwise line 8 in the base class will create an endless loop
- The equals() method in the sub class cannot call the equals() method in the base class otherwise it ends up in an endless loop, too
- Line 11 in the Complex class cannot check against a sub class of Irrational in a different branch (meaning it is not a sub class of Complex, too)
I think we can solve the first two of those problems by throwing more code at it (I’m not sure we want to but I think we can). Here’s how, in the base class we have:
public class PlainNumber {
private int value;
public PlainNumber(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (o == this) {
return true;
}
if (o instanceof PlainNumber) {
if (o.getClass() == getClass()) {
return equalsImpl((PlainNumber)o);
} else if (o.getClass().isAssignableFrom(getClass())) {
return equalsImpl((PlainNumber)o);
} else if (getClass().isAssignableFrom(o.getClass())) {
return ((PlainNumber)o).equalsImpl(this);
} else {
return false;
}
} else {
return false;
}
}
protected boolean equalsImpl(PlainNumber n) {
return getValue() == n.getValue();
}
}
Then in the ComplexNumber subclass:
public class ComplexNumber extends PlainNumber {
private int complexPart;
public ComplexNumber(int rational, int complex) {
super(rational);
complexPart = complex;
}
public int getComplexPart() {
return complexPart;
}
protected boolean equalsImpl(PlainNumber n) {
int nComplexPart = 0;
if (n instanceof ComplexNumber) {
nComplexPart = ((ComplexNumber)n).getComplexPart();
}
return super.equalsImpl(n) &&
getComplexPart() == nComplexPart;
}
}
Additionally for our tests we also create a NoChangePlainNumber class which extends Number but doesn’t override anything:
public class NoChangePlainNumber extends PlainNumber {
public NoChangePlainNumber(int value) {
super(value);
}
}
You can now test semetricality, reflexivity and transitivity between any of these and it holds true. The only problem I have is that new NoChangePlainNumber(1).equals(new ComplexNumber(1, 0)) == false
which, while meeting the requirements for transitivity and semetricality, doesn’t match the mathematical definition. There is a way to solve this though, it’s just so hideously ugly that it may cause you to want to rip your eyeballs out after seeing it. Look below at your own peril:
import java.lang.reflect.*;
public class PlainNumber {
private int value;
public PlainNumber(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (o == this) {
return true;
}
Class thisClass = getClass();
Class otherClass = o.getClass();
// Try to find the classes that actually declare the equals
// method.
try {
Method equals = thisClass.getMethod("equalsImpl",
new Class[] { PlainNumber.class });
thisClass = equals.getDeclaringClass();
} catch (Exception e) {
// There's probably a good case for aborting here instead of
// trying to continue.
thisClass = getClass();
e.printStackTrace();
}
try {
Method equals = otherClass.getMethod("equalsImpl",
new Class[] { PlainNumber.class });
otherClass = equals.getDeclaringClass();
} catch (Exception e) {
// Again, probably should just abort here.
otherClass = o.getClass();
e.printStackTrace();
}
if (o instanceof PlainNumber) {
if (otherClass == thisClass) {
return equalsImpl((PlainNumber)o);
} else if (otherClass.isAssignableFrom(thisClass)) {
return equalsImpl((PlainNumber)o);
} else if (thisClass.isAssignableFrom(otherClass)) {
return ((PlainNumber)o).equalsImpl(this);
} else {
return false;
}
} else {
return false;
}
}
public boolean equalsImpl(PlainNumber n) {
return getValue() == n.getValue();
}
}
Note that I have change equalsImpl to be public (which is bad) simply for convenience. Without that change you’d have to use getDeclaredMethod
instead of getMethod
and that requires you to iterate up the superclass hierarchy yourself which just adds more messy code. I think the concept should be pretty clear though. I would generally suggest that you don’t attempt this firstly because it’s really ugly and hard to understand but more importantly because it’s so unpredictable. If we added the following to NoChangePlainNumber, it’s equals behavior would unexpectedly change:
public boolean equalsImpl(PlainNumber n) {
return super.equalsImpl(n);
}
Since NoChangePlainNumber now overrides equalsImpl it is assumed that it has changed the semantics and so NoChangePlainNumber can no longer be compared with ComplexNumber despite the fact that there has been no effective change to the equalsImpl method.