Подтвердить что ты не робот

Как гарантировать синхронизацию equals() и hashCode()?

Мы пишем класс, который требует очень сложной логики для вычисления equals() и hashCode(). Что-то вдоль линий с:

@Getters @Setters @FieldDefaults(level=AccessLevel.PRIVATE)
public class ExternalData {
  TypeEnum type;
  String data;
  List<ExternalData> children;
} 

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

Мы создали equals() и hashCode(), чтобы отразить все эти правила, но в последнее время столкнулись с проблемой, из-за которой hashCode вышел из синхронизации с равными, в результате чего равные объекты добавляются дважды в HashSet. Я считаю, что HashMap (и HashSet в этом отношении) реализованы таким образом в Java: https://en.wikipedia.org/wiki/Hash_table Реализация сначала помещает объекты в ведра на основе hashCode а затем для каждого ведра проверяется равным. В неудачном сценарии, когда 2 равных объекта перейдут в разные ведра, они никогда не будут сравниваться с помощью equals(). Под "вне синхронизации" здесь я имею в виду, что они входят в разные ведра.

Каков наилучший способ убедиться, что equals и hashCode не выходят из синхронизации?

Edit: Этот вопрос отличается от Какие проблемы следует учитывать при переопределении равных и hashCode в Java? Там они спрашивают об общих рекомендациях, и принятый ответ не применяется в моей ситуации. Они говорят, что "сделать равенства и hashCode последовательными", здесь я спрашиваю, как именно я это делаю.

4b9b3361

Ответ 1

Если алгоритм обхода достаточно сложный, чтобы вы не повторялись, выделите алгоритм на метод, который могут использовать как equals, так и hashCode.

Я вижу два варианта, которые (как это часто бывает) компромисс между широко применимыми и эффективными.

В широком смысле, применимая

Первый вариант - написать довольно общий метод обхода, который принимает функциональный интерфейс и обращается к нему на каждом этапе обхода, поэтому вы можете передать в него лямбду или экземпляр, содержащий фактическую логику, которую вы хотите выполнить, в то время как обходе; шаблон посетителя. Этот интерфейс хотел бы иметь способ сказать "остановить перемещение" (например, поэтому equals может залог, когда он знает, что ответ "не равен" ). Концептуально, что бы выглядело примерно так:

private boolean traverse(Visitor visitor) {
    while (/*still traversing*/) {
        if (!visitor.visitNode(thisNode)) {
            return false;
        }
        /*determine next node to visit and whether done*/
    }
    return true;
}

Затем equals и hashCode используют это для реализации проверки равенства или построения хеш-кода без необходимости знать алгоритм обхода.

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

Проблема заключается в том, что использование этого средства означает выделение экземпляра (или использование лямбда, но тогда вам, вероятно, нужно будет что-то выделять для того, чтобы lamba обновлялось, чтобы отслеживать, что он делает) и совершает множество вызовов методов, Возможно, это хорошо в вашем случае; возможно, это убийца производительности, потому что вашему приложению нужно много использовать equals.: -)

Конкретные и эффективные

... и поэтому вы можете написать что-то конкретное в этом случае, написав то, что имеет логику для equals и hashCode, встроенных в нее. Он будет возвращать хэш-код при использовании hashCode или значение флага для equals (0 = не равно,! 0 = равно). Больше не полезно вообще, но он позволяет создать экземпляр посетителя для передачи служебных служебных/служебных данных/лямбда. Концептуально, это может выглядеть примерно так:

private int equalsHashCodeWorker(Object other, boolean forEquals) {
    int code = 0;

    if (forEquals && other == null) {
        // not equal
    } else {
        while (/*still traversing*/) {
            /*update `code` depending on the results for this node*/
        }
    }

    return code;    
}

Опять же, особенности будут, гм, специфичными для вашего дела, а также вашим руководством по стилю и тому подобным. Некоторые люди сделали бы аргумент other двумя целями (как для флага, так и для "другого" объекта), если equals обрабатывает сам случай other == null и вызывает только этого работника, если у него есть объект null. Я предпочитаю избегать удвоения значения таких аргументов, но вы часто это видите.

Тестирование

В любом случае, если вы находитесь в магазине с культурой тестирования, естественно, вы захотите создать тесты для сложных случаев, которые вы уже видели, а также в других случаях, когда вы видите возможности сбоя.

Боковое примечание о hashCode

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

Ответ 2

Библиотека тестовой библиотеки Guava имеет класс EqualsTester, который можно использовать для написания тестов для реализаций equals() и hashCode().

Добавление тестов помогает вам обеспечить правильность кода в настоящее время, а также гарантирует, что он будет корректным, если/когда вы измените его в будущем.

Ответ 3

одним из вариантов condsider может быть генерация кода. В основном вы пишете список вещей, которые нужно сравнить, и у вас есть программа, которая генерирует как метод equals, так и метод hashcode. Поскольку оба метода генерируются из одного и того же списка вещей для сравнения, они не должны выпадать из синхронизации (при условии, что отдельные элементы не обязательно).

Ответ 4

if a.equals(b), это означает a.hashcode() == b.hashcode().

Однако будьте осторожны. !a.equals(b) делает НЕ подразумевается a.hashcode() != b.hashcode().

Это просто связано с тем, что хеш-столкновения могут быть серьезной проблемой в зависимости от вашего алгоритма и большого числа факторов. В общем случае, если два объекта равны, их хэш-код будет всегда равным. Однако вы не можете определить, равны ли два объекта только при сравнении хэш-кода, так как a.hashode() == b.hashcode() также не подразумевает a.equals(b).