2015-02-05 14 views
7

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:

  1. Jak to obejść?
  2. 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ą?
+3

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

+0

@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

Odpowiedz

2

Odpowiedź na pytanie, dlaczego Porównywalne jest używane dla == jeśli jest to łatwe. To z powodu BigDecimal. Jeśli zrobisz BigDecimal z "1.0" i "1.00" (użyj łańcuchów nie podwójnych!), Otrzymasz dwa BigDecimal, które nie są równe, zgodnie z równymi, ponieważ nie mają tej samej skali. Jednak pod względem wartości są równe, dlatego compareTo będzie je postrzegał jako równy.

Oczywiście jest jeszcze GROOVY-4046, który pokazuje przypadek, w którym bezpośrednie wywołanie compareTo doprowadzi do wyjątku ClassCastException. Ponieważ ten wyjątek jest tutaj nieoczekiwany, postanowiliśmy dodać kontrolę przy możliwości przypisania.

Aby obejść ten problem, można użyć <=> zamiast tego, które już znalazłeś. Tak, nadal przechodzą przez DefaultTypeTransformation, abyś mógł porównać na przykład int i long. Jeśli tego nie chcesz, to bezpośrednie wywołanie compareTo jest drogą do zrobienia. Jeśli źle cię zrozumiałem i chcesz mieć równe szanse, powinieneś zamiast tego nazwać oczywiście równymi.

+0

Dzięki za szczegółową odpowiedź. Używanie compareTo (za pośrednictwem operatora statku kosmicznego) działa, ale nie jest to bardzo wygodne: musiałbym zrobić coś w rodzaju '(v1 <=> v2) == 0', aby przetestować równość. Chciałbym móc użyć "==", aby sprawdzić równość. 'v1.equals (v2)' działałoby, ale wolałbym użyć '==' jeśli to możliwe. Jest to prawdopodobnie głupie pytanie, ponieważ jestem wciąż stosunkowo nowy w stosunku do Groovy, ale czy nie ma sposobu, aby zastąpić operatora "==" dla tych konkretnych klas, aby uczynić je od razu delegować do 'equals' lub' compareTo' jak zdefiniuję ich na zajęciach? – Tagc

+1

W tym przypadku obawiam się, że jedynym sposobem byłoby użycie transformacji AST i zmiana BinaryExpression w MethodCallExpression. Nie jestem pewien, czy chcesz posunąć się tak daleko. Ale zmusiłaś mnie do myślenia, czy wywołanie compareTo jest rzeczywiście potrzebne ... ale nawet wtedy byłoby to dla przyszłej wersji Groovy – blackdrag

+0

Jeśli to jedyny sposób, to na razie trzymam się "równa się" i zaznaczę twoją odpowiedź jako zaakceptowaną. Nie jestem pewien, czy wyeliminowanie wywołania 'compareTo' jest konieczne, ale powiem, że wierzę, że '==' sprawdzi w moim przypadku kontrole, jeśli wymaganie, że' left.getClass(). IsAssignableFrom (right. getClass()) 'jest luźne, aby sprawdzić, czy dwie klasy pochodzą z jakiegoś popularnego interfejsu" Porównywalnego "lub implementującej superklasy. Nie wiem, czy to spowoduje inne problemy, ale uważam, że 'ConcreteVersionA' i' ConcreteVersionB' powinny być porównywalne (z "=="). – Tagc

Powiązane problemy