2014-09-30 5 views
2

Я создал default методы в интерфейсе для реализации equals(Object) и hashCode() предсказуемым образом. Я использую отражение для итерации всех полей типа (класса) для извлечения значений и сравнения их. Код зависит от Apache Commons Lang с его HashCodeBuilder и EqualsBuilder.Java 8 - Методы по умолчанию для equals и hashcode

Дело в том, что мои тесты показывают, что в первый раз, когда я вызываю эти методы, в первом вызове требуется намного больше времени. Таймер использует 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)); 
} 

Причина для установки этих методов в интерфейсе должна быть максимально гибкой. Некоторые из моих классов могут нуждаться в наследовании.

Мой тест показывает, что я могу доверить, что оба хэш-кода и равно всегда возвращают одинаковое значение для объектов с одинаковым внутренним состоянием.

Что бы я хотел знать, если я что-то упустил. И если можно верить в поведение этих методов? (Я знаю, проект Lombok и AutoValue предлагает некоторую помощь в реализации этих методов, но мой клиент не слишком увлекается этими библиотеками).

Любое понимание того, почему это всегда занимает примерно в 5 раз больше времени для вызова метода в первый раз, также будет очень полезно.

ответ

8

Здесь нет ничего особенного с методами default. При первом вызове метода в ранее неиспользуемом классе вызов вызовет загрузку, проверку и инициализацию класса, а выполнение метода начнется в интерпретируемом режиме до того, как начнет работать оптимизатор компилятора/хот-спота JIT. из interface, он будет загружен, а некоторые шаги проверки, выполняемые, когда инициализируется класс, реализующий его, однако остальные шаги все еще откладываются до тех пор, пока они не будут фактически использованы, в вашем случае, когда метод default для interface вызывается в первый раз.

Это нормальное явление на Java, когда выполнение в первый раз занимает больше времени, чем последующие исполнения. В вашем случае вы используете лямбда-выражения, которые имеют дополнительные первоначальные накладные расходы, когда реализация функционального интерфейса будет генерироваться во время выполнения.

Обратите внимание, что ваш код является общим антипаттерном, который существует дольше, чем default методов. Существует не is-a связь между HashAndEquals и классом «внедрение». Вы можете (и должны) предоставить эти два метода полезности как методы static в выделенном классе и использовать import static, если вы хотите использовать эти методы без добавления класса объявления.

Нет преимущества в наследовании этих методов от interface.В конце концов, каждый класс должен переопределить Object.hashCode и Object.equals в любом случае и может сознательно выбирать, использовать ли эти служебные методы или нет.

+0

Благодарим вас за отличный ответ. Кроме того, спасибо за указание отсутствующих отношений 'is-a'. Я также стараюсь поддерживать композицию над наследованием, но в этом случае я потерпел неудачу :). Поскольку выражения lamda генерируют функциональный интерфейс, вы бы рекомендовали использовать простой старый for-loop для производительности? – thomas77

+0

Я бы не назвал это антипаттерном в этом конкретном случае, так как он не применяется к концепции домена: 'equals' и' hashCode' не являются функциями, которые являются частью «домена» класса, а скорее являются техническими требованиями. Используя методы 'default', вы используете синтаксический сахар (mix-in), чтобы добавить чисто технические требования к этим классам. Это намного короче (read: DRY), чтобы написать 'реализует HashAndEquals', чем копировать и вставлять переопределять' equals' и 'hashCode' в каждом отдельном классе. –

+1

@Alexander Langer: кажется, вы пропустили важный аспект, который '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' может не переопределять методы, Вот почему я написал «каждый класс должен переопределять« Object.hashCode »и« Object.equals »в любом случае». Фактически, предполагаемое использование * точно * для копирования и вставки методов 'equals' и' hashCode' (реализаций, вызывающих методы 'default') в каждом отдельном классе. Методы, унаследованные от «интерфейса», не должны вызываться извне, а просто артефакт реализации, экспортируемый через «интерфейс», который является анти-шаблоном. – Holger

Смежные вопросы