我在界面中创建了default
个方法,以可预测的方式实现equals(Object)
和hashCode()
。我使用反射来迭代类型(类)中的所有字段以提取值并进行比较。代码依赖于Apache Commons Lang及其HashCodeBuilder
和EqualsBuilder
。
问题是我的测试告诉我,第一次调用这些方法时,第一次调用需要花费更多时间。计时器使用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();
}
}
如何使用这些方法来实施hashCode
和equals
:
@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总是为具有相同内部状态的对象返回相同的值。
我想知道的是我是否遗漏了什么。如果这些方法的行为可以信任? (我知道项目Lombok和AutoValue为实现这些方法提供了一些帮助,但我的客户对这些库并不太热衷。)
任何关于为什么它总是花费大约5倍的时间来第一次执行方法调用的任何见解也会非常有帮助。
答案 0 :(得分:8)
此处default
方法没有什么特别之处。第一次在以前未使用的类上调用方法时,调用将触发类的类加载,验证和初始化,并且在JIT编译器/热点优化器启动之前,方法的执行将以解释模式启动。 interface
的一个default
,它将被加载,并且在实现它的类被初始化时执行了一些验证步骤,但是其他步骤仍然被推迟到它将被实际使用,在你的情况下interface
1}}第一次调用default
的方法。
在Java中,第一次执行比后续执行需要更多时间,这是正常现象。在您的情况下,您正在使用lambda表达式,当在运行时生成功能接口实现时,这些表达式会有额外的第一次开销。
请注意,您的代码是一种常见的反模式,其存在时间超过HashAndEquals
个方法。 static
与“实现”它的类之间没有 is-a 关系。您可以(并且应该)在专用类中将这两个实用程序方法作为import static
方法提供,如果要在不预先声明声明类的情况下调用这些方法,则使用interface
。
从Object.hashCode
继承这些方法没有任何好处。毕竟,每个类都必须覆盖Object.equals
和{{1}},并且可以故意选择是否使用这些实用方法。