Próbuję opracować projekt w Groovy i odkryłem, że niektóre z moich testów zawodziły w dziwny sposób: mam interfejs Version extends Comparable<Version>
z dwiema podklasami. Oba nadpisują: equals(Object)
i compareTo(Version)
- jeśli jednak spróbuję porównać dwie instancje Version
, które są z różnych typów betonu przy użyciu ==
, sprawdzanie równości nie powiedzie się, mimo że sprawdzane są sprawdzenia jawne equals
i.W Groovy, dlaczego zachowanie "==" zmienia się dla interfejsów rozszerzających porównywalne?
Jeśli usuniemy część extends Comparable<Version>
z Version
, otrzymam oczekiwane zachowanie - ==
spowoduje taki sam wynik jak equals
.
czytałem gdzie indziej, że Groovy delegatom ==
do equals()
chyba że klasa implementuje Comparable
, w którym to przypadku delegatów compareTo
. Jednak znajduję przypadki, w których obie deklarują, że dwa wystąpienia Version
są równe, a mimo to sprawdzenia ==
nie powiedzie się.
Stworzyłem SSCCE, który demonstruje to zachowanie here.
pełny kod znajduje się również poniżej:
// 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)}"
co mam wracać znaczy:
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
Chyba rozumiem, dlaczego tak się dzieje teraz, dzięki JIRA discussion, którą Poundex łączył poniżej.
Od Groovy na DefaultTypeTransformation class, który jest używany do obsługi czeków równość/porównania, zakładam, że metoda compareEqual
najpierw wywoływana, gdy oświadczenie o formie x == y
jest oceniany:
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();
}
Zauważ, że jeśli LHS wyrażenia jest wystąpienie Comparable
, jak w przykładzie I dostarczy porównanie przekazuje się 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));
}
dół w b ottom, metoda ma blok, który deleguje porównanie do metody compareTo
, , ale tylko wtedy, gdy spełnione są pewne warunki. W podanym przeze mnie przykładzie żaden z tych warunków nie jest spełniony, w tym sprawdzanie isAssignableFrom
, ponieważ przykładowe klasy, które zapewniam (i kod w moim projekcie, który daje mi problem) są rodzeństwa, a zatem nie mogą być przypisywane sobie nawzajem.
Chyba rozumiem, dlaczego nie udaje sprawdza teraz, ale nadal jestem zaskoczony przez następujące rzeczy:
- Jak to obejść?
- Jakie są tego przesłanki? Czy to błąd, czy funkcja projektowania? Czy istnieje jakikolwiek powód, dla którego dwie podklasy wspólnej superklasy nie powinny być porównywalne ze sobą?
Wygląda na to, że możesz to zrobić https://jira.codehaus.org/browse/GROOVY-3364 (próbowałem go lokalnie z 2.4.0 i widziałem takie same wyniki jak ty) – Poundex
@Poundex Dzięki za link. Zobaczyłem jeden z komentarzy, w którym "<=>" i "==" przechodzą przez [tutaj] (https://github.com/groovy/groovy-core/blob/master/src/main/org/codehaus/groovy/ runtime/typehandling/DefaultTypeTransformation.java) - szczególnie interesujące są "compareToWithEqualityCheck" i "compareEqual". Wciąż nie jestem do końca pewien, co się dzieje. – Tagc