在Groovy中,为什么'=='的行为会因扩展Comparable的接口而改变? [英] In Groovy, why does the behaviour of '==' change for interfaces extending Comparable?

查看:122
本文介绍了在Groovy中,为什么'=='的行为会因扩展Comparable的接口而改变?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我试图在Groovy中开发一个项目,并且发现我的一些测试以一种奇怪的方式失败:我有一个接口版本扩展了Comparable< Version> 有两个具体的子类。但是,如果我尝试比较 equals(Object) compareTo(Version)使用 == 的不同具体类型的c $ c> Version ,即使显式等于 compareTo 检查通过。



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



我在其他地方读过Groovy委托 = = 到 equals(),除非类实现 Comparable ,在这种情况下,它委托给的compareTo 。但是,我发现两个声明 Version 的两个实例相同但 == 检查失败的情况。



我创建了一个演示此行为的SSCCE 这里



完整的代码也在下面提供:

  //接口扩展Comparable 
interface Super扩展Comparable< Super> {
int getValue()
}

class SubA implements Super {
int getValue(){1}
int compareTo(Super that){this .value< =>如果(!== $ null)返回false
如果(!(o instanceof Super))返回false
this.value == o.value
}
}

class SubB implements Super {
int getValue(){1}
int compareTo(Super that){ this.value< =>如果(!== $ null)返回false
如果(!(o instanceof Super))返回false
this.value == o.value
}
}

//接口不扩展Comparable
接口AnotherSuper {
int getValue()
}

类AnotherSubA实现AnotherSuper {
int getValue(){1}
boolean equals(Object o){
if(o == null)return false
if(!(o instanceof AnotherSuper))返回false
this.value == o.value
}
}

类AnotherSubB实现AnotherSuper {$ b $如果(!(o == null))返回false
如果(!(o instanceof AnotherSuper))返回false
this.value == o.value
}
}


//检查可比较的版本
def a = new SubA()
def b = new SubB()

println可比较的版本相等ity check:$ {a == b}
println显式比较等于check:$ {a.equals(b)}
println显式比较compareTo check:$ {a.compareTo(b )}

//使用非可比较版本进行检查
def anotherA = new AnotherSubA()
def anotherB = new AnotherSubB()

println 非可比较版本相等检查:$ {anotherA == anotherB}
println显式非可比等于支票:$ {anotherA.equals(anotherB)}



我得到的是:

 可比较的版本相等检查:false 
显式可比较等于检查:真
显式可比compareTo检查:0
非可比版本相等检查:true
显式不可比等值check:true

编辑

我想我明白为什么会这样现在发生,感谢Poundex链接到的 JIRA讨论下面。



从Groovy的 DefaultTypeTransformation类,它用于处理相等/比较检查,我假定 compareEqual b










$ b $ p> 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;
}
//在两边处理数组作为效率的特例
Class leftClass = left.getClass();
类rightClass = right.getClass();
if(leftClass.isArray()&& rightClass.isArray()){
return compareArrayEqual(left,right); $()b
$ b if(leftClass.isArray()&& leftClass.getComponentType()。isPrimitive()){
left = primitiveArrayToList(left); (rightClass.isArray()&& rightClass.getComponentType()。isPrimitive()){
right = primitiveArrayToList(right);
}
if ((Object []),((List)right);}
返回DefaultGroovyMethods.equals((Object [])left,(List)right); ((List)left,(Object [])right);
}
if(left instanceof List&& right instanceof Object []){
return DefaultGroovyMethods.equals ((list)left,(List)right);
}
if(left instanceof List&& right instanceof List){
return DefaultGroovyMethods.equals (Map.Entry&&& right instanceof Map.Entry的实例){
Object k1 =((Map.Entry)left).getKey();
}
if
Object k2 =((Map.Entry)right).getKey(); (k1 == k2 ||(k1!= null&& k1.equals(k2))){
Object v1 =((Map.Entry)left).getValue();
if(k1 == k2 ||
Object v2 =((Map.Entry)right).getValue();
if(v1 == v2 ||(v1!= null&&&&& DefaultTypeTransformation.compareEqual(v1,v2)))
return true;
}
返回false;
}
return((Boolean)InvokerHelper.invokeMethod(left,equals,right))。booleanValue();

$ / code>

请注意,如果表达式的LHS是<$ c $的一个实例c> Comparable ,就像我提供的例子那样,比较被委托给 compareToWithEqualityCheck

  private static int compareToWithEqualityCheck(Object left,Object right,boolean equalityCheckOnly){$ b $ if(left == right){
return 0;
}
if(left == null){
return -1;
}
else if(right == null){
return 1;
}
if(left instanceof Number){
return DefaultGroovyMethods。{
if(left instanceof Number){
if compareTo((Number)left,castToNumber(right)); $(b)b
if(isValidCharacterString(right)){
return DefaultGroovyMethods.compareTo((Number)left,ShortTypeHandling.castToChar(right)); $(b)

$ b其他if(left instanceof Character){
if(isValidCharacterString(right)){
return DefaultGroovyMethods.compareTo((Character)left,ShortTypeHandling。 castToChar(右));
}
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), (数字)右);


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()); $!
$ b if(!equalityCheckOnly || left.getClass()。isAssignableFrom(right.getClass())
||(right.getClass()!= Object.class&&比较可比=(可比较)right.getClass()。isAssignableFrom(left.getClass()))// GROOVY-4046
||(GString&&剩下;
return comparable.compareTo(right);



if(equalityCheckOnly){
return -1; //不是0
}
抛出新的GroovyRuntimeException(
MessageFormat.format(无法将值{0}与值''{1}''和{2}进行比较,值为'' {3}'',
left.getClass()。getName(),
left,
right.getClass()。getName(),
right));
}

接近底部时,该方法有一个块,将比较委托给 compareTo 方法,但只有在满足某些条件的情况下。在我提供的例子中,没有满足这些条件,包括 isAssignableFrom 检查,因为我提供的示例类(以及我的项目中给我的问题的代码)是兄弟姐妹,因此不能分配给另一个。



我想我明白为什么检查失败了,但我仍然困惑不解以下内容:


  1. 如何解决此问题?

  2. 背后的基本原理是什么?这是一个错误还是设计特征?是否有任何理由说明为什么一个普通超类的两个子类不应该相互比较?
  3. 为什么Comparable为==而使用的答案是否容易。这是因为BigDecimal。如果您使BigDecimal超出1.0和1.00(使用字符串不加倍!),您会得到两个不等于等于的BigDecimal,因为它们不具有相同的比例。价值方面他们是平等的,这就是为什么compareTo会看到他们是平等的。



    当然还有 GROOVY-4046 ,它显示了一个直接调用compareTo会导致ClassCastException的情况。由于这个例外是意想不到的,所以我们决定添加一个可分配性检查。



    为了解决这个问题,您可以使用< => 代替您已经找到的。是的,他们仍然通过 DefaultTypeTransformation ,所以你可以比较一个int和long。如果你不想那么做,那么直接调用compareTo就是一条路。如果我误解了你的意思,而你想真正拥有平等的话,那么你应该调用equals来代替。


    I'm trying to develop a project in Groovy and I've found some of my tests failing in an odd way: I have an interface Version extends Comparable<Version> with two concrete subclasses. Both override equals(Object) and compareTo(Version) - however, if I try to compare two instances of Version that are of different concrete types using ==, the equality check fails even though explicit equals and compareTo checks pass.

    If I remove the extends Comparable<Version> part of Version, I get the expected behaviour - == gives the same result as equals would.

    I've read elsewhere that Groovy delegates == to equals() unless the class implements Comparable, in which case it delegates to compareTo. However, I'm finding cases where both declare two instances of Version to be equal and yet the == checks fail.

    I've created an SSCCE that demonstrates this behaviour here.

    The full code is also provided below:

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

    What I'm getting back is:

    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
    I think I understand why this happens now, thanks to the JIRA discussion that Poundex linked to below.

    From Groovy's DefaultTypeTransformation class, which is used to handle equality/comparison checks, I assume that the compareEqual method is first called when a statement of the form x == y is being evaluated:

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

    Notice that if the LHS of the expression is an instance of Comparable, as it is in the example I provide, the comparison is delegated to 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));
    }
    

    Down near the bottom, the method has a block that delegates the comparison to the compareTo method, but only if certain conditions are satisfied. In the example I provide, none of these conditions are satisfied, including the isAssignableFrom check, since the example classes I provide (and the code in my project that's giving me the problem) are siblings, and therefore not assignable to one another.

    I suppose I understand why the checks are failing now, but I'm still puzzled over the following things:

    1. How do I get around this?
    2. What's the rationale behind this? Is this a bug or a design feature? Is there any reason why two subclasses of a common super class should not be comparable with one another?

    解决方案

    The answer to why Comparable is used for == if existing is easy. It is because of BigDecimal. If you make a BigDecimal out of "1.0" and "1.00" (use Strings not doubles!) you get two BigDecimal that are not equal according to equals, because they don't have the same scale. Value-wise they are equal though, which is why compareTo will see them as equal.

    Then of course there is also GROOVY-4046, which shows a case in which just directly calling compareTo will lead to a ClassCastException. Since this exception is unexpected here we decided to add an check for assignability.

    To get around this you can use <=> instead which you already found. Yes, they still go through DefaultTypeTransformation so you can compare for example an int and an long. If you don't want that either, then directly calling compareTo is the way to go. If I misunderstood you and you want to actually have equals, well, then you should call equals of course instead.

    这篇关于在Groovy中,为什么'=='的行为会因扩展Comparable的接口而改变?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆