2015-02-05 9 views
7

Sto provando a sviluppare un progetto in Groovy e ho trovato alcuni dei miei test che hanno fallito in modo strano: ho un'interfaccia Version extends Comparable<Version> con due sottoclassi concrete. Entrambi sostituiscono equals(Object) e compareTo(Version) - tuttavia, se provo a confrontare due istanze di Version di tipi diversi di calcestruzzo utilizzando ==, il controllo di uguaglianza non riesce anche se i passaggi espliciti sono equals e compareTo.In Groovy, perché il comportamento di '==' cambia per le interfacce che estendono Comparable?

Se rimuovo la parte extends Comparable<Version> di Version, ottengo il comportamento previsto - == dà lo stesso risultato di equals avrebbe fatto.

ho letto altrove delegati che Groovy ==-equals() meno che la classe implementa Comparable, nel qual caso i delegati al compareTo. Tuttavia, sto trovando casi in cui entrambi dichiarano due istanze di Version uguali e tuttavia i controlli == non riescono.

Ho creato un SSCCE che dimostra questo comportamento here.

Il codice completo è inoltre fornito di seguito:

// 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)}" 

quello che sto ricevendo indietro è:

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
Credo di capire perché questo accade ora, grazie ad il JIRA discussion che Poundex ha collegato a sotto.

Da Groovy di DefaultTypeTransformation class, che viene utilizzato per gestire i controlli uguaglianza/confronto, presumo che il metodo compareEqual viene prima chiamato quando una dichiarazione della forma x == y è in corso di valutazione:

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(); 
} 

Si noti che se il LHS dell'espressione è un'istanza di Comparable, come è nell'esempio fornisco, il confronto viene delegata a 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)); 
} 

il basso vicino al b ottom, il metodo ha un blocco che delega il confronto al metodo compareTo, ma solo se sono soddisfatte determinate condizioni. Nell'esempio che fornisco, nessuna di queste condizioni è soddisfatta, incluso il controllo isAssignableFrom, poiché le classi di esempio che fornisco (e il codice nel mio progetto che mi sta dando il problema) sono fratelli, e quindi non assegnabili tra loro.

Credo di capire perché i controlli non riescono ora, ma io sono ancora perplesso sopra le seguenti cose:

  1. Come posso ottenere intorno a questo?
  2. Qual è la logica alla base di questo? È un bug o una funzionalità di progettazione? C'è qualche ragione per cui due sottoclassi di una super classe comune non dovrebbero essere comparabili l'una con l'altra?
+3

Sembra che tu possa avere questo https://jira.codehaus.org/browse/GROOVY-3364 (l'ho provato localmente con la versione 2.4.0 e ho visto i tuoi stessi risultati) – Poundex

+0

@Poundex Grazie per il link. Ho visto uno dei commenti menzionare che '<=>' e '==' passare attraverso [qui] (https://github.com/groovy/groovy-core/blob/master/src/main/org/codehaus/groovy/ runtime/typehandling/DefaultTypeTransformation.java) - di particolare interesse sono 'compareToWithEqualityCheck' e' compareEqual'. Non sono ancora sicuro di cosa stia succedendo, comunque. – Tagc

risposta

2

La risposta al motivo per cui Paragonabile viene utilizzato per == se esistente è semplice. È a causa di BigDecimal. Se crei un BigDecimal su "1.0" e "1.00" (usa Stringhe non doppie!) Ottieni due BigDecimal che non sono uguali secondo gli uguali, perché non hanno la stessa scala. Per quanto riguarda il valore, sono uguali, ed è per questo che confrontareTo li vedrà come uguali.

Quindi ovviamente c'è anche GROOVY-4046, che mostra un caso in cui solo chiamando direttamente compareTo porterà a ClassCastException. Poiché questa eccezione è inaspettata, abbiamo deciso di aggiungere un assegno per l'assegnazione.

Per aggirare questo è possibile utilizzare <=> invece che hai già trovato. Sì, passano ancora attraverso DefaultTypeTransformation in modo da poter confrontare per esempio un int e un lungo. Se non vuoi neanche quello, allora chiama direttamente compareTo è la strada da percorrere. Se ti ho frainteso e vuoi davvero avere degli eguali, beh, allora dovresti chiamare equamente, ovviamente.

+0

Grazie per la risposta dettagliata. L'uso di compareTo (tramite l'operatore dell'astronave) funziona, ma non è molto conveniente: avrei bisogno di fare qualcosa del tipo '(v1 <=> v2) == 0' per verificare l'uguaglianza. Quello che vorrei è poter usare '==' per verificare l'uguaglianza. 'v1.equals (v2)' funzionerebbe, ma preferirei usare '==' se possibile. Questa è probabilmente una domanda stupida perché sono ancora relativamente nuovo per Groovy, ma non c'è un modo per sovrascrivere l'operatore '==' per queste classi particolari per renderli immediatamente delegati a 'equals' o' compareTo' mentre definisco loro nelle classi? – Tagc

+1

In questo caso, temo che l'unico modo sarebbe utilizzare una trasformazione AST e modificare BinaryExpression in MethodCallExpression. Non sei sicuro di voler andare così lontano. Ma mi hai fatto pensare se chiamare il confrontoPer davvero è necessario ... ma anche allora, questo sarebbe per una futura versione di Groovy – blackdrag

+0

Se è l'unico modo allora rimarrò con 'equals' per ora e segnerà la tua risposta come accettata. Non sono sicuro se eliminare la chiamata a 'compareTo' è necessario, ma dirò che credo che '==' verrebbe passato nel mio caso se il requisito che' left.getClass(). ÈAssignableFrom (a destra. getClass()) 'è rilassato semplicemente controllando che le due classi derivino da alcune comuni estensioni dell'interfaccia' Comparable' o implementando la superclasse. Non so se questo creerà altri problemi, ma credo che 'ConcreteVersionA' e' ConcreteVersionB' dovrebbero essere reciprocamente comparabili (con '=='). – Tagc

Problemi correlati