在 Groovy 中,为什么 '==' 的行为对于扩展 Comparable 的接口会发生变化?
In Groovy, why does the behaviour of '==' change for interfaces extending Comparable?
我正在尝试在 Groovy 中开发一个项目,我发现我的一些测试以一种奇怪的方式失败了:我有一个接口 Version extends Comparable<Version>
和两个具体的子 class是的。两者都覆盖 equals(Object)
和 compareTo(Version)
- 但是,如果我尝试使用 ==
比较不同具体类型的 Version
的两个实例,即使显式 equals
和 compareTo
检查通过。
如果我删除 Version
的 extends Comparable<Version>
部分,我会得到预期的行为 - ==
给出与 equals
相同的结果。
我在别处读到,Groovy 将 ==
委托给 equals()
,除非 class 实现了 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
编辑
我想我明白为什么现在会发生这种情况,多亏了 Poundex 链接到下面的 JIRA discussion。
来自 Groovy 的 DefaultTypeTransformation class,用于处理 equality/comparison 检查,我假设 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));
}
在底部附近,该方法有一个块将比较委托给 compareTo
方法,但前提是满足某些条件。在我提供的示例中,none 这些条件得到满足,包括 isAssignableFrom
检查,因为我提供的示例 classes(以及我项目中的代码给我带来了问题)是兄弟姐妹,因此不能转让给彼此。
我想我明白为什么现在检查失败了,但我仍然对以下事情感到困惑:
- 我该如何解决这个问题?
- 这背后的原理是什么?这是错误还是设计功能?有什么理由说明一个普通超级 class 的两个子 class 不应该相互比较?
为什么 Comparable 用于 == if existing 的答案很简单。这是因为 BigDecimal。如果您从“1.0”和“1.00”中创建一个 BigDecimal(使用字符串而不是双打!),您将得到两个不相等的 BigDecimal,因为它们不具有相同的比例。尽管在价值方面它们是相等的,这就是为什么 compareTo 将它们视为相等的原因。
当然还有GROOVY-4046,就是直接调用compareTo会抛出ClassCastException的情况。由于这里的异常是意外的,我们决定添加对可分配性的检查。
要解决这个问题,您可以使用您已经找到的 <=>
。是的,它们仍然经过 DefaultTypeTransformation
因此您可以比较 int 和 long。如果您也不想这样做,那么直接调用 compareTo 是可行的方法。如果我误解了你,而你实际上想要 equals,那么你当然应该调用 equals。
我正在尝试在 Groovy 中开发一个项目,我发现我的一些测试以一种奇怪的方式失败了:我有一个接口 Version extends Comparable<Version>
和两个具体的子 class是的。两者都覆盖 equals(Object)
和 compareTo(Version)
- 但是,如果我尝试使用 ==
比较不同具体类型的 Version
的两个实例,即使显式 equals
和 compareTo
检查通过。
如果我删除 Version
的 extends Comparable<Version>
部分,我会得到预期的行为 - ==
给出与 equals
相同的结果。
我在别处读到,Groovy 将 ==
委托给 equals()
,除非 class 实现了 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
编辑
我想我明白为什么现在会发生这种情况,多亏了 Poundex 链接到下面的 JIRA discussion。
来自 Groovy 的 DefaultTypeTransformation class,用于处理 equality/comparison 检查,我假设 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));
}
在底部附近,该方法有一个块将比较委托给 compareTo
方法,但前提是满足某些条件。在我提供的示例中,none 这些条件得到满足,包括 isAssignableFrom
检查,因为我提供的示例 classes(以及我项目中的代码给我带来了问题)是兄弟姐妹,因此不能转让给彼此。
我想我明白为什么现在检查失败了,但我仍然对以下事情感到困惑:
- 我该如何解决这个问题?
- 这背后的原理是什么?这是错误还是设计功能?有什么理由说明一个普通超级 class 的两个子 class 不应该相互比较?
为什么 Comparable 用于 == if existing 的答案很简单。这是因为 BigDecimal。如果您从“1.0”和“1.00”中创建一个 BigDecimal(使用字符串而不是双打!),您将得到两个不相等的 BigDecimal,因为它们不具有相同的比例。尽管在价值方面它们是相等的,这就是为什么 compareTo 将它们视为相等的原因。
当然还有GROOVY-4046,就是直接调用compareTo会抛出ClassCastException的情况。由于这里的异常是意外的,我们决定添加对可分配性的检查。
要解决这个问题,您可以使用您已经找到的 <=>
。是的,它们仍然经过 DefaultTypeTransformation
因此您可以比较 int 和 long。如果您也不想这样做,那么直接调用 compareTo 是可行的方法。如果我误解了你,而你实际上想要 equals,那么你当然应该调用 equals。