I am trying to develop a project in Groovy, and I found that some of my tests fail in an odd way: I have a Version extends Comparable<Version> interface with two specific subclasses. Both override equals(Object) and compareTo(Version) - however, if I try to compare two Version instances that have different specific types using == , the equality check fails, although explicit equals and compareTo checks pass.
If I extends Comparable<Version> part of Version , I get the expected behavior - == gives the same result as equals .
I read elsewhere that Groovy delegates == to equals() if the class does not implement Comparable , in which case it delegates compareTo . However, I find cases where both declare two instances of Version equal, and yet the == check fails.
I created SSCCE that demonstrates this behavior here .
The full code is also given 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 get 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 is happening now, thanks to the JIRA discussion, which is related to Poundex below.
From Groovy's DefaultTypeTransformation class , which is used to handle equality / comparison checks, I assume that the compareEqual method compareEqual first called when an instruction of the form x == y is 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(); }
Note that if the LHS expression is an instance of Comparable , as 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()))
Down, the method has a block that delegates the comparison to the compareTo method, but only if certain conditions are met. In the above example, none of these conditions is met, including the isAssignableFrom check, because the proposed class classes (and the code in my project that gives me the problem) are siblings and therefore cannot be assigned to each other.
I suppose I understand why the checks don't work, but I'm still puzzled by the following things:
- How do I get around this?
- What is the reason for this? Is this a bug or a design feature? Is there a reason why two subclasses of a common superclass should not be comparable with each other?