Groovy == operator does not reach Java equals(o) method - how is it possible?
Asked Answered
H

1

6

I have following Java class:

import org.apache.commons.lang3.builder.EqualsBuilder;

public class Animal {

    private final String name;
    private final int numLegs;

    public Animal(String name, int numLegs) {
        this.name = name;
        this.numLegs = numLegs;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }

        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        Animal animal = (Animal)o;

        return new EqualsBuilder().append(numLegs, animal.numLegs)
            .append(name, animal.name)
            .isEquals();
    }

}

And the following Spock test:

import spock.lang.Specification

class AnimalSpec extends Specification {

    def 'animal with same name and numlegs should be equal'() {
        when:
        def animal1 = new Animal("Fluffy", 4)
        def animal2 = new Animal("Fluffy", 4)
        def animal3 = new Animal("Snoopy", 4)
        def notAnAnimal = 'some other object'
        then:
        animal1 == animal1
        animal1 == animal2
        animal1 != animal3
        animal1 != notAnAnimal
    }

}

Then when running coverage, the first statement animal1 == animal1 does not reach equals(o) method:

Line 16 not covered by test

Is there any reason why Groovy/Spock are not running the first statement? I assume a micro-optimization but then when I would make a mistake like

@Override
public boolean equals(Object o) {
    if (this == o) {
        return false;
    }

    if (o == null || getClass() != o.getClass()) {
        return false;
    }

    Animal animal = (Animal)o;

    return new EqualsBuilder().append(numLegs, animal.numLegs)
        .append(name, animal.name)
        .isEquals();
}

the test is still green. Why is this happening?

Edit on a Sunday morning: I did some more testing and found out that it is even not an optimization but causing overhead on even a significant amount of invocations, when running this test:

class AnimalSpec extends Specification {

    def 'performance test of == vs equals'() {
        given:
        def animal = new Animal("Fluffy", 4)
        when:
        def doubleEqualsSignBenchmark = 'benchmark 1M invocation of == on'(animal)
        def equalsMethodBenchmark = 'benchmark 1M invocation of .equals(o) on'(animal)
        println "1M invocation of == took ${doubleEqualsSignBenchmark} ms and 1M invocations of .equals(o) took ${equalsMethodBenchmark}ms"
        then:
        doubleEqualsSignBenchmark < equalsMethodBenchmark
    }

    long 'benchmark 1M invocation of == on'(Animal animal) {
        return benchmark {
            def i = {
                animal == animal
            }
            1.upto(1_000_000, i)
        }
    }

    long 'benchmark 1M invocation of .equals(o) on'(Animal animal) {
        return benchmark {
            def i = {
                animal.equals(animal)
            }
            1.upto(1_000_000, i)
        }
    }

    def benchmark = { closure ->
        def start = System.currentTimeMillis()
        closure.call()
        def now = System.currentTimeMillis()
        now - start
    }
}

I expected this test to succeed but I ran it several times and it was never green...

1M invocation of == took 164 ms and 1M invocations of .equals(o) took 139ms

Condition not satisfied:

doubleEqualsSignBenchmark < equalsMethodBenchmark
|                         | |
164                       | 139
                          false

When even more increasing to 1B invocations, the optimization becomes visible:

1B invocation of == took 50893 ms and 1B invocations of .equals(o) took 75568ms
Headline answered 28/9, 2018 at 12:56 Comment(1)
Btw, running a benchmark without a JVM warm-up won't give you a proper results. You can use Groovy GBench to run more accurate benchmarks, e.g. gist.github.com/wololock/a2e54b77545dc4632ab633c9f3b4c1fd It shows that animal == animal vs. animal.equals(animal) is executed almost without any cost on a JVM that is properly warmed up.Blinnie
B
11

This optimization exists because the following expression:

animal1 == animal1

Groovy translates to the following method call:

ScriptBytecodeAdapter.compareEqual(animal1, animal1)

Now, if we take a look at this method's source code we will find out that in the first step this method uses good old Java's object reference comparison - if both sides of the expression point to the same reference, it simply returns true and equals(o) or compareTo(o) (in case of comparing objects that implement Comparable<T> interface) methods are not get invoked:

public static boolean compareEqual(Object left, Object right) {
    if (left==right) return true;
    Class<?> leftClass = left==null?null:left.getClass();
    Class<?> rightClass = right==null?null:right.getClass();

    // ....
}

In your case both, left and right variables point to the same object reference, so the first check in the method matches and true gets returned.

If you put a breakpoint in this place (ScriptBytecodeAdapter.java line 685) you will see that debugger is reaching that point and it returns true from the first line of this method.

Decompiling Groovy code

As a nice exercise you can take a look at the following example. This is a simple Groovy script (called Animal_script.groovy) that uses Animal.java class and does object comparison:

def animal1 = new Animal("Fluffy", 4)
def animal2 = new Animal("Fluffy", 4)
def animal3 = new Animal("Snoopy", 4)

println animal1 == animal1

If you compile it and open Animal_script.class file in the IntelliJ IDEA (so it can be decompiled back to Java), you will see something like this:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import groovy.lang.Binding;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.callsite.CallSite;

public class Animal_script extends Script {
    public Animal_script() {
        CallSite[] var1 = $getCallSiteArray();
    }

    public Animal_script(Binding context) {
        CallSite[] var2 = $getCallSiteArray();
        super(context);
    }

    public static void main(String... args) {
        CallSite[] var1 = $getCallSiteArray();
        var1[0].call(InvokerHelper.class, Animal_script.class, args);
    }

    public Object run() {
        CallSite[] var1 = $getCallSiteArray();
        Object animal1 = var1[1].callConstructor(Animal.class, "Fluffy", 4);
        Object animal2 = var1[2].callConstructor(Animal.class, "Fluffy", 4);
        Object animal3 = var1[3].callConstructor(Animal.class, "Snoopy", 4);
        return var1[4].callCurrent(this, ScriptBytecodeAdapter.compareEqual(animal1, animal1));
    }
}

As you can see, animal1 == animal1 is seen by Java runtime as ScriptBytecodeAdapter.compareEqual(animal1, animal1)).

Hope it helps.

Blinnie answered 28/9, 2018 at 13:19 Comment(3)
A very good answer to a good question! (Sorry for writing a kudos-only comment, I usually avoid it and just upvote. But there is an exception to every rule.)Plumbo
Thanks @Szymon Stepniak for the very detailed explanation. I find this topic very interesting and did some further investigation. I was a bit surprised to see that their "optimization" is slower than the normal method. It's funny to see that those so called 'fancy pants' languages probably spent quit some time on this special case optimization but still fail to make it great...Headline
Groovy is a dynamic language, so its method invocation comes with an overhead. If you are looking for Groovy optimization I would recommend reading about Groovy static compilation feature - groovy-lang.org/semantics.html#_static_compilation It is useful if you are not interested in Groovy's dynamic nature and you are willing to sacrifice it for better efficiency. In the end of the day JIT compiler will do its job anyway and will optimize your code based on its usage.Blinnie

© 2022 - 2024 — McMap. All rights reserved.