2015-02-05 1 views
7

Я пытаюсь разработать проект в Groovy, и некоторые из моих тестов оказались нечетными: у меня есть интерфейс Version extends Comparable<Version> с двумя конкретными подклассами. Оба переопределяют equals(Object) и compareTo(Version). Однако, если я попытаюсь сравнить два экземпляра Version, которые имеют разные типы бетона, используя ==, проверка равенства не выполняется, хотя явные equals и compareTo проверяет пропуск.В Groovy, почему поведение '==' изменяется для интерфейсов, расширяющих Comparable?

Если удалить extends Comparable<Version>Version части, я получаю ожидаемое поведение - == дает тот же результат, как equals бы.

Я читал в другом месте, что Groovy делегаты == к equals(), если класс не реализует Comparable, в этом случае делегатов compareTo. Тем не менее, я нахожу случаи, когда оба объявляют два экземпляра Version равными, и все же ошибки == не выполняются.

Я создал SSCCE, который демонстрирует это поведение here.

Полный код также приводится ниже:

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

Что я получаю обратно есть:

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
Я думаю, я понимаю, почему это происходит сейчас, благодаря JIRA discussion, связанный с Poundex ниже.

С Groovy, DefaultTypeTransformation class, который используется для обработки проверки равенства/сравнения, я полагаю, что метод compareEqual сначала вызывается, когда утверждение вида x == y проводится оценка:

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

Обратите внимание, что если LHS выражения является экземпляром Comparable, как в примере я обеспечиваю, сравнение делегированы 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)); 
} 

вниз вблизи б ottom, метод имеет блок, который делегирует сравнение с методом compareTo, , но только при выполнении определенных условий. В приведенном ниже примере ни одно из этих условий не выполняется, в том числе проверка isAssignableFrom, так как примеры классов, которые я предоставляю (и код в моем проекте, который дает мне проблему), являются siblings и поэтому не могут быть назначены друг другу.

Я предполагаю, что я понимаю, почему чеки не удается сейчас, но я до сих пор ломают голову над следующими вещами:

  1. Как обойти это?
  2. В чем причина этого? Это ошибка или функция дизайна? Есть ли причина, почему два подкласса общего суперкласса не должны быть сопоставимы друг с другом?
+3

Похоже, что у вас может быть этот https://jira.codehaus.org/browse/GROOVY-3364 (я пробовал его локально с помощью 2.4.0 и видел те же результаты, что и вы) – Poundex

+0

@Poundex Спасибо за ссылку. Я заметил, что в одном из комментариев упоминается, что «<=>» и «==» проходят через [здесь] (https://github.com/groovy/groovy-core/blob/master/src/main/org/codehaus/groovy/ runtime/typehandling/DefaultTypeTransformation.java) - особый интерес представляют «compareToWithEqualityCheck» и «compareEqual». Тем не менее, я все еще не уверен, что происходит. – Tagc

ответ

2

Ответ на вопрос, почему Comparable используется для ==, если существующее легко. Это из-за BigDecimal. Если вы делаете BigDecimal из «1.0» и «1.00» (используйте строки не удваиваются!) Вы получаете два BigDecimal, которые не равны по равным, потому что они не имеют одинакового масштаба. Понятно, что они равны, и поэтому compareTo будет считать их равными.

Тогда, конечно, есть также GROOVY-4046, в котором показан случай, в котором только непосредственно вызов compareTo приведет к исключению ClassCastException. Поскольку это исключение неожиданно здесь, мы решили добавить проверку на назначение.

Чтобы обойти это, вы можете использовать <=>, вместо этого вы уже нашли. Да, они все еще проходят через DefaultTypeTransformation, поэтому вы можете сравнить, например, int и long. Если вы этого не хотите, то прямое обращение к compareTo - это путь. Если я неправильно понял вас, и вы хотите на самом деле иметь равные, ну, то вместо этого вы должны называть равные.

+0

Спасибо за подробный ответ. Использование compareTo (через оператора космического корабля) действительно работает, но это не очень удобно: мне нужно будет сделать что-то вроде '(v1 <=> v2) == 0', чтобы проверить равенство. Я хотел бы использовать '==' для проверки равенства. 'v1.equals (v2)' будет работать, но я предпочел бы использовать '==', если это возможно. Вероятно, это тупой вопрос, поскольку я все еще относительно новичок в Groovy, но не могу ли я переопределить оператор '==' для этих конкретных классов, чтобы они сразу же делегировали 'equals' или' compareTo', как я определяю их в классах? – Tagc

+1

В этом случае я боюсь, что единственным способом было бы использовать преобразование AST и изменить BinaryExpression в MethodCallExpression. Не уверен, что ты хочешь зайти так далеко. Но вы заставили меня подумать, действительно ли нужно называть compareTo ... но даже тогда это будет для будущей версии Groovy. – blackdrag

+0

Если это единственный способ, то я буду придерживаться 'equals' и покажу ваш ответ как принятый. Я не уверен, нужно ли отменять вызов 'compareTo', но я скажу, что, по-моему, проверки« == »будут проходить в моем случае, если требование, что' left.getClass() .AssignableFrom (справа. getClass()) 'расслаблен, чтобы просто проверить, что два класса проистекают из некоторого распространенного интерфейса« Comparable »или реализации суперкласса. Я не знаю, создадут ли это другие проблемы, но я чувствую, что «ConcreteVersionA» и «ConcreteVersionB» должны быть взаимно сопоставимы (с «==»). – Tagc

Смежные вопросы