Java 8 - equals和hashcode的默认方法

tho*_*s77 2 behavior equals hashcode java-8 default-method

我已经default在接口中创建了用于实现equals(Object)hashCode()以可预测方式的方法.我使用反射来迭代类型(类)中的所有字段以提取值并进行比较.代码依赖于Apache Commons Lang及其HashCodeBuilderEqualsBuilder.

问题是我的测试告诉我,第一次调用这些方法时,第一次调用需要花费更多的时间.计时器使用System.nanoTime().以下是日志中的示例:

Time spent hashCode: 192444
Time spent hashCode: 45453
Time spent hashCode: 48386
Time spent hashCode: 50951
Run Code Online (Sandbox Code Playgroud)

实际代码:

public interface HashAndEquals {

    default <T> int getHashCode(final T type) {
        final List<Field> fields = Arrays.asList(type.getClass().getDeclaredFields());
        final HashCodeBuilder builder = new HashCodeBuilder(31, 7);
        fields.forEach( f -> {
            try {
                f.setAccessible(true);
                builder.append(f.get(type));
            } catch (IllegalAccessException e) {
                throw new GenericException(e.toString(), 500);
            }
        });
        return builder.toHashCode();
    }

    default <T, K> boolean isEqual(final T current, final K other) {
        if(current == null || other == null) {
            return false;
        }
        final List<Field> currentFields = Arrays.asList(current.getClass().getDeclaredFields());
        final List<Field> otherFields = Arrays.asList(other.getClass().getDeclaredFields());
        final IsEqual isEqual = new IsEqual();
        isEqual.setValue(true);
        currentFields.forEach(c -> otherFields.forEach(o -> {
            c.setAccessible(true);
            o.setAccessible(true);
            try {
                if (o.getName().equals(c.getName())) {
                    if (!o.get(other).equals(c.get(current))) {
                        isEqual.setValue(false);
                    }
                }
            } catch (IllegalAccessException e) {
                isEqual.setValue(false);
            }
        }));
        return isEqual.getValue();
    }
}
Run Code Online (Sandbox Code Playgroud)

如何使用这些方法来实现hashCodeequals:

@Override
public int hashCode() {
    return getHashCode(this);
}

@Override
public boolean equals(Object obj) {
    return obj instanceof Step && isEqual(this, obj);
}
Run Code Online (Sandbox Code Playgroud)

测试示例:

    @Test
public void testEqualsAndHashCode() throws Exception {
    Step step1 = new Step(1, Type.DISPLAY, "header 1", "description");
    Step step2 = new Step(1, Type.DISPLAY, "header 1", "description");
    Step step3 = new Step(2, Type.DISPLAY, "header 2", "description");
    int times = 1000;
    long total = 0;

    for(int i = 0; i < times; i++) {
        long start = System.nanoTime();
        boolean equalsTrue = step1.equals(step2);
        long time = System.nanoTime() - start;
        total += time;
        System.out.println("Time spent: " + time);
        assertTrue( equalsTrue );
    }
    System.out.println("Average time: " + total / times);

    for(int i = 0; i < times; i++) {
        assertEquals( step1.hashCode(), step2.hashCode() );
        long start = System.nanoTime();
        System.out.println(step1.hashCode() + " = " + step2.hashCode());
        System.out.println("Time spent hashCode: " + (System.nanoTime() - start));
    }
    assertFalse( step1.equals(step3) );
} 
Run Code Online (Sandbox Code Playgroud)

将这些方法放在界面中的原因是尽可能灵活.我的一些类可能需要继承.

我的测试表明我可以相信hashcode和equals总是为具有相同内部状态的对象返回相同的值.

我想知道的是,如果我遗失了什么.如果这些方法的行为可以信任?(我知道项目LombokAutoValue提供了一些实现这些方法的帮助,但我的客户对这些库并不太热衷).

任何关于为什么它总是花费大约5倍的时间来第一次执行方法调用的任何见解也将是非常有帮助的.

Hol*_*ger 8

这里的方法没什么特别之处default.第一次在以前未使用的类上调用方法时,调用将触发类的类加载,验证和初始化,并且在JIT编译器/热点优化器启动之前,方法的执行将以解释模式启动.一个interface,它将被加载,并在实现它的类初始化时执行一些验证步骤,但是其他步骤仍然被推迟,直到它实际使用,在你的情况下调用一个default方法的时候interface第一次.

在Java中,正常现象是第一次执行比后续执行花费更多时间.在您的情况下,您正在使用lambda表达式,当在运行时生成功能接口实现时,这些表达式会有额外的第一次开销.

请注意,您的代码是一种常见的反模式,其存在时间比default方法长.有没有is-a的关系HashAndEquals和阶级"实行"它.您可以(并且应该)将这两个实用程序方法作为static方法提供给专用类,并且import static如果要在不预先声明声明类的情况下调用这些方法,则使用它们.

继承这些方法没有任何好处interface.毕竟,每个类都必须覆盖Object.hashCode并且Object.equals无论如何都可以故意选择是否使用这些实用方法.