I'm trying to develop a project in Groovy and I've found some of my tests failing in an odd way: I have an interface Version extends Comparable<Version>
with two concrete subclasses. Both override equals(Object)
and compareTo(Version)
- however, if I try to compare two instances of Version
that are of different concrete types using ==
, the equality check fails even though explicit equals
and compareTo
checks pass.
If I remove the extends Comparable<Version>
part of Version
, I get the expected behaviour - ==
gives the same result as equals
would.
I've read elsewhere that Groovy delegates ==
to equals()
unless the class implements Comparable
, in which case it delegates to compareTo
. However, I'm finding cases where both declare two instances of Version
to be equal and yet the ==
checks fail.
I've created an SSCCE that demonstrates this behaviour here.
The full code is also provided below:
// Interface extending Comparable
interface Super extends Comparable<Super> {
int getValue()
}
class SubA implements Super {
int getValue() { 1 }
int compareTo(Super that) { this.value <=> that.value }
boolean equals(Object o) {
if (o == null) return false
if (!(o instanceof Super)) return false
this.value == o.value
}
}
class SubB implements Super {
int getValue() { 1 }
int compareTo(Super that) { this.value <=> that.value }
boolean equals(Object o) {
if (o == null) return false
if (!(o instanceof Super)) return false
this.value == o.value
}
}
// Interface not extending Comparable
interface AnotherSuper {
int getValue()
}
class AnotherSubA implements AnotherSuper {
int getValue() { 1 }
boolean equals(Object o) {
if (o == null) return false
if (!(o instanceof AnotherSuper)) return false
this.value == o.value
}
}
class AnotherSubB implements AnotherSuper {
int getValue() { 1 }
boolean equals(Object o) {
if (o == null) return false
if (!(o instanceof AnotherSuper)) return false
this.value == o.value
}
}
// Check with comparable versions
def a = new SubA()
def b = new SubB()
println "Comparable versions equality check: ${a == b}"
println "Explicit comparable equals check: ${a.equals(b)}"
println "Explicit comparable compareTo check: ${a.compareTo(b)}"
// Check with non-comparable versions
def anotherA = new AnotherSubA()
def anotherB = new AnotherSubB()
println "Non-comparable versions equality check: ${anotherA == anotherB}"
println "Explicit non-comparable equals check: ${anotherA.equals(anotherB)}"
What I'm getting back is:
Comparable versions equality check: false
Explicit comparable equals check: true
Explicit comparable compareTo check: 0
Non-comparable versions equality check: true
Explicit non-comparable equals check: true
EDIT
I think I understand why this happens now, thanks to the JIRA discussion that Poundex linked to below.
From Groovy's DefaultTypeTransformation class, which is used to handle equality/comparison checks, I assume that the compareEqual
method is first called when a statement of the form x == y
is being evaluated:
public static boolean compareEqual(Object left, Object right) {
if (left == right) return true;
if (left == null || right == null) return false;
if (left instanceof Comparable) {
return compareToWithEqualityCheck(left, right, true) == 0;
}
// handle arrays on both sides as special case for efficiency
Class leftClass = left.getClass();
Class rightClass = right.getClass();
if (leftClass.isArray() && rightClass.isArray()) {
return compareArrayEqual(left, right);
}
if (leftClass.isArray() && leftClass.getComponentType().isPrimitive()) {
left = primitiveArrayToList(left);
}
if (rightClass.isArray() && rightClass.getComponentType().isPrimitive()) {
right = primitiveArrayToList(right);
}
if (left instanceof Object[] && right instanceof List) {
return DefaultGroovyMethods.equals((Object[]) left, (List) right);
}
if (left instanceof List && right instanceof Object[]) {
return DefaultGroovyMethods.equals((List) left, (Object[]) right);
}
if (left instanceof List && right instanceof List) {
return DefaultGroovyMethods.equals((List) left, (List) right);
}
if (left instanceof Map.Entry && right instanceof Map.Entry) {
Object k1 = ((Map.Entry)left).getKey();
Object k2 = ((Map.Entry)right).getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = ((Map.Entry)left).getValue();
Object v2 = ((Map.Entry)right).getValue();
if (v1 == v2 || (v1 != null && DefaultTypeTransformation.compareEqual(v1, v2)))
return true;
}
return false;
}
return ((Boolean) InvokerHelper.invokeMethod(left, "equals", right)).booleanValue();
}
Notice that if the LHS of the expression is an instance of Comparable
, as it is in the example I provide, the comparison is delegated to compareToWithEqualityCheck
:
private static int compareToWithEqualityCheck(Object left, Object right, boolean equalityCheckOnly) {
if (left == right) {
return 0;
}
if (left == null) {
return -1;
}
else if (right == null) {
return 1;
}
if (left instanceof Comparable) {
if (left instanceof Number) {
if (right instanceof Character || right instanceof Number) {
return DefaultGroovyMethods.compareTo((Number) left, castToNumber(right));
}
if (isValidCharacterString(right)) {
return DefaultGroovyMethods.compareTo((Number) left, ShortTypeHandling.castToChar(right));
}
}
else if (left instanceof Character) {
if (isValidCharacterString(right)) {
return DefaultGroovyMethods.compareTo((Character)left, ShortTypeHandling.castToChar(right));
}
if (right instanceof Number) {
return DefaultGroovyMethods.compareTo((Character)left,(Number)right);
}
}
else if (right instanceof Number) {
if (isValidCharacterString(left)) {
return DefaultGroovyMethods.compareTo(ShortTypeHandling.castToChar(left),(Number) right);
}
}
else if (left instanceof String && right instanceof Character) {
return ((String) left).compareTo(right.toString());
}
else if (left instanceof String && right instanceof GString) {
return ((String) left).compareTo(right.toString());
}
if (!equalityCheckOnly || left.getClass().isAssignableFrom(right.getClass())
|| (right.getClass() != Object.class && right.getClass().isAssignableFrom(left.getClass())) //GROOVY-4046
|| (left instanceof GString && right instanceof String)) {
Comparable comparable = (Comparable) left;
return comparable.compareTo(right);
}
}
if (equalityCheckOnly) {
return -1; // anything other than 0
}
throw new GroovyRuntimeException(
MessageFormat.format("Cannot compare {0} with value ''{1}'' and {2} with value ''{3}''",
left.getClass().getName(),
left,
right.getClass().getName(),
right));
}
Down near the bottom, the method has a block that delegates the comparison to the compareTo
method, but only if certain conditions are satisfied. In the example I provide, none of these conditions are satisfied, including the isAssignableFrom
check, since the example classes I provide (and the code in my project that's giving me the problem) are siblings, and therefore not assignable to one another.
I suppose I understand why the checks are failing now, but I'm still puzzled over the following things:
The answer to why Comparable is used for == if existing is easy. It is because of BigDecimal. If you make a BigDecimal out of "1.0" and "1.00" (use Strings not doubles!) you get two BigDecimal that are not equal according to equals, because they don't have the same scale. Value-wise they are equal though, which is why compareTo will see them as equal.
Then of course there is also GROOVY-4046, which shows a case in which just directly calling compareTo will lead to a ClassCastException. Since this exception is unexpected here we decided to add an check for assignability.
To get around this you can use <=>
instead which you already found. Yes, they still go through DefaultTypeTransformation
so you can compare for example an int and an long. If you don't want that either, then directly calling compareTo is the way to go. If I misunderstood you and you want to actually have equals, well, then you should call equals of course instead.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With