Groovy ==运算符没有达到Java等于(o)方法 - 它怎么可能?

Dav*_*ave 6 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();
    }

}
Run Code Online (Sandbox Code Playgroud)

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

}
Run Code Online (Sandbox Code Playgroud)

然后在运行coverage时,第一个语句animal1 == animal1没有到达equals(o)方法:

第16行未被测试覆盖

有没有理由为什么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();
}
Run Code Online (Sandbox Code Playgroud)

测试仍然是绿色的.为什么会这样?

在星期天早上编辑: 我做了一些测试,发现它甚至不是优化,但在运行此测试时,即使是大量的调用也会造成开销:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

我希望这个测试能够成功但是我跑了好几次并且它从来都不是绿色的......

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

Condition not satisfied:

doubleEqualsSignBenchmark < equalsMethodBenchmark
|                         | |
164                       | 139
                          false
Run Code Online (Sandbox Code Playgroud)

当更多地增加到1B调用时,优化变得可见:

1B invocation of == took 50893 ms and 1B invocations of .equals(o) took 75568ms
Run Code Online (Sandbox Code Playgroud)

Szy*_*iak 11

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

animal1 == animal1
Run Code Online (Sandbox Code Playgroud)

Groovy转换为以下方法调用:

ScriptBytecodeAdapter.compareEqual(animal1, animal1)
Run Code Online (Sandbox Code Playgroud)

现在,如果我们看一下这个方法的源代码,我们会发现在第一步中这个方法使用了旧的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();

    // ....
}
Run Code Online (Sandbox Code Playgroud)

在你的情况下,两个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
Run Code Online (Sandbox Code Playgroud)

如果您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));
    }
}
Run Code Online (Sandbox Code Playgroud)

如您animal1 == animal1所见,Java运行时将其视为ScriptBytecodeAdapter.compareEqual(animal1, animal1)).

希望能帮助到你.

  • 一个好问题的一个很好的答案!_(很抱歉只写了一个赞美的评论,我通常会避免它而只是投票.但是每个规则都有例外.)_ (2认同)