Search code examples
groovycomparisonoperator-overloading

Groovy == doesn't act like it should according to specification


So according to the official documentation == behaves like compareTo when called on a Comparable Object.

Now I have a class like this:

class LargeNum implements Serializable, Comparable<BigInteger>

which defines the method

@Override
int compareTo(BigInteger o) throws NullPointerException, ClassCastException {
    println "compared"
    return compareStuff()
}

You'd expect this bit of code:

LargeNum t = new LargeNum(1)
println t == new BigInteger(1)

to print out "compared" and call compareStuff, though it doesn't. The only output I receive is: false

I'm genuinely confused on why this is the way it is. Overriding equals doesn't quite solve the problem either.


Solution

  • Documentation is not clear in this case - .compareTo(Object obj) gets called instead of .equals(Object obj) if you use == operator between two objects of the same type T that implements Comparable<T>. In your example class LargeNum implements Comparable<BigInteger> - in this case

    new LargeNum(1) == new LargeNum(1)
    

    will call .equals(Object obj) method and not .compareTo(Object obj).

    If you implement Comparable<LargeNum> and you compare

    new LargeNum(1) == new LargeNum(1)
    

    the .compareTo(Object obj) kicks in. Consider following example:

    class LargeNum implements Comparable<LargeNum> {
    
      private final BigInteger number
    
      LargeNum(BigInteger number) {
        this.number = number
      }
    
      @Override
      int compareTo(LargeNum num) {
        println "compared"
        return number <=> num.number
      }
    }
    
    LargeNum num1 = new LargeNum(1)
    LargeNum num2 = new LargeNum(1)
    
    println num1 == num2
    

    Running this example produces following output:

    compared
    true
    

    Can I override == operator with .equals(Object obj) method in this case?

    Short answer - nope. To understand why and what happens we need to dig a little bit into the bytecode. Following part of the code:

    LargeNum num1 = new LargeNum(1)
    LargeNum num2 = new LargeNum(1)
    BigInteger bigInt1 = new BigInteger(1)
    
    println num1 == num2
    
    println num1 == bigInt1
    

    is represented as following bytecode decompiled to Java:

    LargeNum num1 = (LargeNum)ScriptBytecodeAdapter.castToType(var1[1].callConstructor(LargeNum.class, 1), LargeNum.class);
    LargeNum num2 = (LargeNum)ScriptBytecodeAdapter.castToType(var1[2].callConstructor(LargeNum.class, 1), LargeNum.class);
    BigInteger bigInt1 = (BigInteger)ScriptBytecodeAdapter.castToType(var1[3].callConstructor(BigInteger.class, 1), BigInteger.class);
    var1[4].callCurrent(this, ScriptBytecodeAdapter.compareEqual(num1, num2));
    return var1[5].callCurrent(this, ScriptBytecodeAdapter.compareEqual(num1, bigInt1));
    

    It shows that num1 == bitInt1 is actually ScriptBytecodeAdapter.compareEqual(num1, bigInt1). If we take a look at the source code we will find out that this method in our case executes method:

    DefaultTypeTransformation.compareEqual(left, right);
    

    Now, if we take a look what the implementation of this method looks like we will find something like this:

    public static boolean compareEqual(Object left, Object right) {
        if (left == right) return true;
        if (left == null) return right instanceof NullObject;
        if (right == null) return left instanceof NullObject;
        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();
        // some other stuff here
    }
    

    It shows that for any Comparable objects following part gets executed:

    if (left instanceof Comparable) {
        return compareToWithEqualityCheck(left, right, true) == 0;
    }
    

    The implementation of this method reveals that if two objects are the same type or they can be cast to the common type, .compareTo(Object obj) method gets triggered. Otherwise it just returns -1. And this is what happens if we try to compare LargeNum and BigInteger - there is no common denominator between these two classes so at least for Groovy they are not comparable.

    I don't know if this is bug or not that there is no fallback to .equals(Object obj) method. Alternatively you can always call specific methods directly, for instance:

    num1.equals(bigInt1)
    

    or

    num1.compareTo(bigInt1) // num1 <=> bigInt1
    

    However I understand that this behavior of == operator for Comparable might be pretty counter intuitive and annoying. Especially if you define your class to implement Comparable<Object> - I would expect that any object gets passed to overridden .compareTo(Object obj) but it's not :/

    Hope it helps.