3

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 倍的时间的任何见解也将非常有帮助。

4

1 回答 1

9

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

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

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

interface. 毕竟,每个类都必须重写Object.hashCodeObject.equals并且可以有意识地选择是否使用这些实用方法。

于 2014-09-30T15:02:36.620 回答