Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

java

groovy

spock

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
like image 718
Dave Avatar asked Sep 28 '18 12:09

Dave


1 Answers

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.

like image 132
Szymon Stepniak Avatar answered Sep 30 '22 13:09

Szymon Stepniak