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
animal == animal
vs.animal.equals(animal)
is executed almost without any cost on a JVM that is properly warmed up. – Blinnie