我在用于以可预测的方式实现equals(Object)
和hashCode()
的接口中创建了default
方法。我使用反射来迭代类型(类)中的所有字段以提取值并进行比较。代码取决于Apache Commons Lang及其HashCodeBuilder
和EqualsBuilder
。Java 8 - 用于等号和哈希码的默认方法
问题是,我的测试显示我第一次打电话给这些方法时,第一次调用需要更多时间。计时器使用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倍的时间才能第一次执行方法调用也会非常有帮助。
谢谢你的一个很好的答案。另外,感谢您指出缺少的“is-a”关系。我也试图赞成构成而不是继承,但在这种情况下我失败了:)。由于lamda表达式生成一个功能接口,你会推荐使用普通的旧for循环来提高性能吗? – thomas77 2014-09-30 15:27:13
在这个特殊情况下,我不会将它称为反模式,因为它不适用于域概念:equals和hashCode不是属于类的“域”一部分的函数,而是技术要求。通过使用“默认”方法,人们使用语法糖(mix-in)为这些类添加纯技术要求。与在每个类中复制和粘贴覆盖“equals”和“hashCode”相比,编写'implements HashAndEquals'要短得多(读为DRY)。 – 2014-09-30 19:49:05
@Alexander Langer:似乎你错过了''default''方法无法覆盖从'java.lang.Object'继承的方法的重要方面。这就是为什么我写道“无论如何,每个类必须覆盖Object.hashCode'和Object.equals'。事实上,预期的用途是*精确地*复制和粘贴每个类中的equals和hashCode方法(调用'default'方法的实现)。从'interface'继承的方法并不意味着从外部调用,而只是通过'interface'导出的实现工件,这是一种反模式。 – Holger 2014-09-30 20:12:30