Java 8 - equals和hashcode的默认方法

时间:2014-09-30 14:36:30

标签: equals java-8 hashcode behavior 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

实际代码:

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

如何使用这些方法来实施hashCodeequals

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

@Override
public boolean equals(Object obj) {
    return obj instanceof Step && isEqual(this, obj);
}

测试示例:

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

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

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

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

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

1 个答案:

答案 0 :(得分:8)

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

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

请注意,您的代码是一种常见的反模式,其存在时间超过HashAndEquals个方法。 static与“实现”它的类之间没有 is-a 关系。您可以(并且应该)在专用类中将这两个实用程序方法作为import static方法提供,如果要在不预先声明声明类的情况下调用这些方法,则使用interface

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