Groovy == оператор не достигает Java метод equals (o) - как это возможно? - PullRequest
0 голосов
/ 28 сентября 2018

У меня следующий класс Java:

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();
    }

}

И следующий тест Спока:

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
    }

}

Тогда при выполнении покрытия первый оператор animal1 == animal1 не достигает equals(o)метод:

Line 16 not covered by test

Есть ли какая-либо причина, по которой Groovy / Spock не выполняет первый оператор?Я предполагаю микрооптимизацию, но тогда, когда я совершу ошибку , как

@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();
}

, тест все еще зеленый.Почему это происходит?

Редактировать в воскресенье утром: Я провел еще какое-то тестирование и обнаружил, что это даже не оптимизация, а накладные расходы даже наЗначительное количество вызовов при выполнении этого теста:

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
    }
}

Я ожидал, что этот тест пройдет успешно, но я запускал его несколько раз, и он никогда не был зеленым ...

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

Condition not satisfied:

doubleEqualsSignBenchmark < equalsMethodBenchmark
|                         | |
164                       | 139
                          false

Когда дажеПри увеличении количества вызовов до 1B оптимизация становится видимой:

1B invocation of == took 50893 ms and 1B invocations of .equals(o) took 75568ms

1 Ответ

0 голосов
/ 28 сентября 2018

Эта оптимизация существует, потому что следующее выражение:

animal1 == animal1

Groovy переводит в следующий вызов метода:

ScriptBytecodeAdapter.compareEqual(animal1, animal1)

Теперь, если мы посмотрим на этот методИсходный код мы обнаружим, что на первом этапе этот метод использует сравнение ссылок на старые добрые объекты Java - если обе стороны выражения указывают на одну и ту же ссылку, он просто возвращает true и equals(o) или compareTo(o)(в случае сравнения объектов, которые реализуют Comparable<T> интерфейс) методы не вызываются:

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();

    // ....
}

В вашем случае обе переменные left и right указывают на одну и ту же ссылку на объект, поэтомупервая проверка в методе соответствует и true возвращается.

Если вы установите точку останова в этом месте (ScriptBytecodeAdapter.java строка 685), вы увидите, что отладчик достигает этой точки, и он возвращает true изпервая строка этого метода.

Декомпиляция Groovy-кода

В качестве приятного упражнения вы можете взглянуть на следующий пример.Это простой скрипт Groovy (называемый Animal_script.groovy), который использует класс Animal.java и выполняет сравнение объектов:

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

println animal1 == animal1

Если вы скомпилируете его и откроете файл Animal_script.class в IntelliJ IDEA (так что он можетбыть декомпилированным обратно в Java), вы увидите что-то вроде этого:

//
// 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));
    }
}

Как вы можете видеть, animal1 == animal1 во время выполнения Java рассматривается как ScriptBytecodeAdapter.compareEqual(animal1, animal1)).

Надеюсь, это поможет.

...