2014-09-30 52 views
2

我在用于以可预测的方式实现equals(Object)hashCode()的接口中创建了default方法。我使用反射来迭代类型(类)中的所有字段以提取值并进行比较。代码取决于Apache Commons Lang及其HashCodeBuilderEqualsBuilderJava 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(); 
    } 
} 

如何被用来实现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倍的时间才能第一次执行方法调用也会非常有帮助。

回答

8

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

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

请注意,您的代码是一种常见的反模式,其存在时间比default方法长。关于HashAndEquals和类“实施”它之间的关系是没有的。您可以(也应该)在专用类中提供这两种实用方法作为static方法,如果要在不预先声明声明类的情况下调用这些方法,请使用import static

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

+0

谢谢你的一个很好的答案。另外,感谢您指出缺少的“is-a”关系。我也试图赞成构成而不是继承,但在这种情况下我失败了:)。由于lamda表达式生成一个功能接口,你会推荐使用普通的旧for循环来提高性能吗? – thomas77 2014-09-30 15:27:13

+0

在这个特殊情况下,我不会将它称为反模式,因为它不适用于域概念:equals和hashCode不是属于类的“域”一部分的函数,而是技术要求。通过使用“默认”方法,人们使用语法糖(mix-in)为这些类添加纯技术要求。与在每个类中复制和粘贴覆盖“equals”和“hashCode”相比,编写'implements HashAndEquals'要短得多(读为DRY)。 – 2014-09-30 19:49:05

+1

@Alexander Langer:似乎你错过了''default''方法无法覆盖从'java.lang.Object'继承的方法的重要方面。这就是为什么我写道“无论如何,每个类必须覆盖Object.hashCode'和Object.equals'。事实上,预期的用途是*精确地*复制和粘贴每个类中的equals和hashCode方法(调用'default'方法的实现)。从'interface'继承的方法并不意味着从外部调用,而只是通过'interface'导出的实现工件,这是一种反模式。 – Holger 2014-09-30 20:12:30