在Groovy中,为什么'=='的行为会因扩展Comparable的接口而改变? [英] In Groovy, why does the behaviour of '==' change for interfaces extending 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讨论下面。
请注意,如果表达式的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
检查,因为我提供的示例类(以及我的项目中给我的问题的代码)是兄弟姐妹,因此不能分配给另一个。
我想我明白为什么检查失败了,但我仍然困惑不解以下内容:
- 如何解决此问题?
- 背后的基本原理是什么?这是一个错误还是设计特征?是否有任何理由说明为什么一个普通超类的两个子类不应该相互比较?
- How do I get around this?
- 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?
为什么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:
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屋!