Я создал 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 раз больше времени для вызова метода в первый раз, также будет очень полезно.
Благодарим вас за отличный ответ. Кроме того, спасибо за указание отсутствующих отношений 'is-a'. Я также стараюсь поддерживать композицию над наследованием, но в этом случае я потерпел неудачу :). Поскольку выражения lamda генерируют функциональный интерфейс, вы бы рекомендовали использовать простой старый for-loop для производительности? – thomas77
Я бы не назвал это антипаттерном в этом конкретном случае, так как он не применяется к концепции домена: 'equals' и' hashCode' не являются функциями, которые являются частью «домена» класса, а скорее являются техническими требованиями. Используя методы 'default', вы используете синтаксический сахар (mix-in), чтобы добавить чисто технические требования к этим классам. Это намного короче (read: DRY), чтобы написать 'реализует HashAndEquals', чем копировать и вставлять переопределять' equals' и 'hashCode' в каждом отдельном классе. –
@Alexander Langer: кажется, вы пропустили важный аспект, который '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' может не переопределять методы, Вот почему я написал «каждый класс должен переопределять« Object.hashCode »и« Object.equals »в любом случае». Фактически, предполагаемое использование * точно * для копирования и вставки методов 'equals' и' hashCode' (реализаций, вызывающих методы 'default') в каждом отдельном классе. Методы, унаследованные от «интерфейса», не должны вызываться извне, а просто артефакт реализации, экспортируемый через «интерфейс», который является анти-шаблоном. – Holger