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:
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
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With