在Groovy中,为什么'==';扩展接口的更改?

在Groovy中,为什么'==';扩展接口的更改?,groovy,equality,Groovy,Equality,我正在尝试用Groovy开发一个项目,我发现我的一些测试以一种奇怪的方式失败了:我有一个接口版本扩展了两个具体的子类。两者都覆盖等于(对象)和比较到(版本)——但是,如果我尝试使用==比较不同具体类型的版本的两个实例,即使显式等于和比较到检查通过,相等性检查也会失败 如果我删除版本的部分,我会得到预期的行为-=给出与等于相同的结果 我在别处读到Groovy将==委托给equals(),除非类实现了Comparable,在这种情况下它将委托给compareTo。然而,我发现两个Version的实例

我正在尝试用Groovy开发一个项目,我发现我的一些测试以一种奇怪的方式失败了:我有一个接口
版本扩展了两个具体的子类。两者都覆盖
等于(对象)
比较到(版本)
——但是,如果我尝试使用
==
比较不同具体类型的
版本的两个实例,即使显式
等于
比较到
检查通过,相等性检查也会失败

如果我删除
版本
部分,我会得到预期的行为-
=
给出与
等于
相同的结果

我在别处读到Groovy将
==
委托给
equals()
,除非类实现了
Comparable
,在这种情况下它将
委托给compareTo
。然而,我发现两个
Version
的实例声明相等,而
=
检查失败

我已经创建了一个SSCCE来演示这种行为

下面还提供了完整代码:

// 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)}"
编辑
我想我现在明白了为什么会发生这种情况,多亏了下面链接的Poundex

根据用于处理相等/比较检查的Groovy,我假设在计算
x==y
形式的语句时,首先调用
compareEqual
方法:

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));
}
在底部附近,该方法有一个块,它将比较委托给
compareTo
方法,但只有在满足某些条件的情况下。在我提供的示例中,这些条件都不满足,包括
isAssignableFrom
检查,因为我提供的示例类(以及我的项目中给出问题的代码)是兄弟类,因此不能相互分配

我想我现在明白了为什么检查失败了,但我仍然对以下事情感到困惑:

  • 我该怎么做
  • 这背后的理由是什么?这是一个bug还是一个设计特性?有没有任何理由可以解释为什么一个公共超类的两个子类不应该相互比较

  • 如果存在,那么为什么将Comparable用于==的答案很简单。这是因为BigDecimal。如果你用“1.0”和“1.00”组成一个BigDecimal(使用字符串而不是双精度!),你会得到两个BigDecimal,它们根据相等值是不相等的,因为它们的比例不同。但从价值角度看,它们是平等的,这就是为什么compareTo会认为它们是平等的

    当然还有,这显示了直接调用compareTo将导致ClassCastException的情况。由于此异常在这里是意外的,因此我们决定添加一个可分配性检查


    要解决这个问题,您可以使用您已经找到的
    。是的,它们仍然要经过
    DefaultTypeTransformation
    ,因此您可以比较例如int和long。如果您也不想这样做,那么直接调用compareTo是一种方法。如果我误解了您的意思,您希望实际拥有equals,那么您当然应该调用equals。

    看起来您可能拥有此功能(我在本地试用了2.4.0,结果与您相同)@Poundex感谢链接。我看到其中一条评论提到了“”和“==”,特别感兴趣的是
    compareToWithEqualityCheck
    comparequal
    。不过我还是不确定到底发生了什么。谢谢你的详细回复。使用compareTo(通过spaceship操作符)确实有效,但并不十分方便:我需要做一些类似
    (v1 v2)==0的事情来测试等式。我希望能够使用“==”检查是否相等
    v1.equals(v2)
    可以工作,但如果可能的话,我更愿意使用
    =
    。这可能是一个愚蠢的问题,因为我对Groovy还比较陌生,但我是否有办法覆盖这些特定类的“==”操作符,使它们在类中定义时立即委托给
    equals
    compareTo
    ?在这种情况下,恐怕唯一的方法就是使用AST转换和将BinaryExpression更改为MethodCallExpression。我不确定你想走那么远。但你让我想如果真的需要打电话给compareTo。。。但即便如此,这也适用于未来的Groovy版本。如果这是唯一的方法,那么我现在将坚持使用
    equals
    ,并将您的答案标记为已接受。我不确定是否有必要取消对
    compareTo
    的调用,但我认为如果
    left.getClass().isAssignableFrom(right.getClass())的要求是
    放松到只检查两个类,这两个类派生自一些通用的
    可比较的
    ——扩展接口或实现超类。我不知道这是否会产生其他问题,但我觉得
    ConcreteVersionA
    ConcreteVersionB
    应该可以相互比较(使用“==”)。
    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));
    }