Groovy ==运算符未达到Java equals(o)方法-怎么可能?

时间:2018-09-28 12:56:41

标签: java groovy spock

我有以下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();
    }

}

以及以下Spock测试:

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
    }

}

然后,当运行coverage时,第一个语句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 :(得分:10)

存在此优化是因为以下表达式:

animal1 == animal1

Groovy转换为以下方法调用:

ScriptBytecodeAdapter.compareEqual(animal1, animal1)

现在,如果我们看一下this method's source code,我们会发现,在第一步中,此方法使用了很好的旧Java对象引用比较-如果表达式的两边都指向同一个引用,则简单地返回trueequals(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();

    // ....
}

在您的情况下,leftright变量都指向相同的对象引用,因此方法中的第一次检查匹配,并且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

如果您对其进行编译并在IntelliJ IDEA中打开Animal_script.class文件(以便可以将其反编译为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))

希望有帮助。