Лучшая реализация метода hashCode для коллекции


299

Как мы выбираем наилучшую реализацию hashCode()метода для коллекции (при условии, что метод equals был корректно переопределен)?


2
с Java 7+, я думаю, Objects.hashCode(collection)должно быть идеальным решением!
— Diablo

3
@Diablo Я не думаю , что ответы на вопрос , вообще - этот метод просто возвращает collection.hashCode()( hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/src/share/... )
— cbreezier

Ответы:


438

Лучшая реализация? Это сложный вопрос, потому что это зависит от модели использования.

Практически во всех случаях разумная хорошая реализация была предложена в « Эффективной Java» Джоша Блоха в статье 8 (второе издание). Лучше всего искать это там, потому что автор объясняет, почему подход хорош.

Короткая версия

  1. Создайте int resultи назначьте ненулевое значение.

  2. Для каждого поля, f проверенного в equals()методе, рассчитайте хеш-код c:

    • Если поле f является boolean: вычислить (f ? 0 : 1);
    • Если поле F является byte, char, shortили int: вычислить (int)f;
    • Если поле f является long: вычислить (int)(f ^ (f >>> 32));
    • Если поле f является float: вычислить Float.floatToIntBits(f);
    • Если поле f является double: вычислить Double.doubleToLongBits(f)и обработать возвращаемое значение, как любое длинное значение;
    • Если поле f является объектом : используйте результат hashCode()метода или 0 if f == null;
    • Если поле f является массивом : рассмотрите каждое поле как отдельный элемент и вычислите значение хеш-функции рекурсивным способом и объедините значения, как описано далее.
  3. Объедините значение хеша cс result:

    result = 37 * result + c
  4. Возвращение result

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


45
Да, мне особенно любопытно, откуда происходит число 37.
— Кип

17
Я использовал пункт 8 книги Джоша Блоха «Эффективная Ява».
— дмейстер

39
@dma_k Причина использования простых чисел и метода, описанного в этом ответе, заключается в том, чтобы гарантировать, что вычисленный хэш-код будет уникальным . При использовании не простых чисел вы не можете гарантировать это. Неважно, какое простое число вы выберете, в числе 37 нет ничего волшебного (жаль, что 42 не простое число, а?)
— Симон Форсберг,

34
@ SimonAndréForsberg Ну, вычисляемый хеш-код не всегда может быть уникальным :) Это хеш-код. Однако у меня возникла идея: простое число имеет только один множитель, в то время как не простое имеет как минимум два. Это создает дополнительную комбинацию для оператора умножения, чтобы привести к тому же хешу, то есть вызвать столкновение.
— dma_k


140

Если вы довольны реализацией Effective Java, рекомендованной dmeister, вы можете использовать библиотечный вызов вместо собственного:

@Override
public int hashCode() {
    return Objects.hashCode(this.firstName, this.lastName);
}

Это требует либо Guava ( com.google.common.base.Objects.hashCode) или стандартной библиотеки в Java 7 ( java.util.Objects.hash), но работает так же.


8
Если у кого-то нет веских причин не использовать их, то в любом случае следует обязательно их использовать. (Формулировка более сильная, как это ИМХО должна быть сформулирована.) Применяются типичные аргументы для использования стандартных реализаций / библиотек (лучшие практики, хорошо протестированные, менее подверженные ошибкам и т. Д.).
— Киссаки

7
@ justin.hughey ты, кажется, запутался. Единственный случай, который вы должны переопределить, hashCode- это если у вас есть пользовательский интерфейс equals, и именно для этого и предназначены эти библиотечные методы. В документации совершенно ясно об их поведении по отношению к equals. Реализация библиотеки не претендует на то, чтобы освободить вас от знания характеристик правильной hashCodeреализации - эти библиотеки упрощают реализацию такой соответствующей реализации в большинстве случаев, где equalsона переопределяется.
— Бакар

6
Для всех разработчиков Android, которые смотрят на класс java.util.Objects, он был представлен только в API 19, поэтому убедитесь, что вы работаете на KitKat или выше, в противном случае вы получите NoClassDefFoundError.
— Эндрю Келли

3
Лучший ответ IMO, хотя в качестве примера я бы предпочел выбрать java.util.Objects.hash(...)метод JDK7, чем метод guava com.google.common.base.Objects.hashCode(...). Я думаю, что большинство людей предпочли бы стандартную библиотеку за дополнительную зависимость.
— Malte Skoruppa

2
Если есть два или более аргументов и если какой-либо из них является массивом, результат может быть не таким, как вы ожидаете, потому что hashCode()для массива это просто его java.lang.System.identityHashCode(...).
— Старикофф

59

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


4
+1 Хорошее практическое решение. Решение dmeister более всеобъемлющее, но я склонен забывать обрабатывать пустые значения, когда пытаюсь написать хеш-коды самостоятельно.
— Quantum7

1
+1 Согласен с Quantum7, но я бы сказал, что очень хорошо понимать, что делает реализация, сгенерированная Eclipse, и откуда она получает подробности своей реализации.
— jwir3

15
Извините, но ответы, включающие «функциональность, предоставляемую [некоторыми IDE]», не очень актуальны в контексте языка программирования в целом. Существуют десятки IDE, и это не отвечает на вопрос ... а именно потому, что это больше касается алгоритмического определения и напрямую связано с реализацией equals () - о чем IDE ничего не будет знать.
— Даррелл Тига

57

Хотя это связано с Androidдокументацией (Wayback Machine) и моим собственным кодом на Github , в целом это будет работать для Java. Мой ответ - это расширение ответа dmeister с помощью простого кода, который намного легче читать и понимать.

@Override 
public int hashCode() {

    // Start with a non-zero constant. Prime is preferred
    int result = 17;

    // Include a hash for each field.

    // Primatives

    result = 31 * result + (booleanField ? 1 : 0);                   // 1 bit   » 32-bit

    result = 31 * result + byteField;                                // 8 bits  » 32-bit 
    result = 31 * result + charField;                                // 16 bits » 32-bit
    result = 31 * result + shortField;                               // 16 bits » 32-bit
    result = 31 * result + intField;                                 // 32 bits » 32-bit

    result = 31 * result + (int)(longField ^ (longField >>> 32));    // 64 bits » 32-bit

    result = 31 * result + Float.floatToIntBits(floatField);         // 32 bits » 32-bit

    long doubleFieldBits = Double.doubleToLongBits(doubleField);     // 64 bits (double) » 64-bit (long) » 32-bit (int)
    result = 31 * result + (int)(doubleFieldBits ^ (doubleFieldBits >>> 32));

    // Objects

    result = 31 * result + Arrays.hashCode(arrayField);              // var bits » 32-bit

    result = 31 * result + referenceField.hashCode();                // var bits » 32-bit (non-nullable)   
    result = 31 * result +                                           // var bits » 32-bit (nullable)   
        (nullableReferenceField == null
            ? 0
            : nullableReferenceField.hashCode());

    return result;

}

РЕДАКТИРОВАТЬ

Как правило, когда вы переопределяете hashcode(...), вы также хотите переопределить equals(...). Так что для тех, кто будет или уже реализован equals, вот хорошая ссылка от моего Github ...

@Override
public boolean equals(Object o) {

    // Optimization (not required).
    if (this == o) {
        return true;
    }

    // Return false if the other object has the wrong type, interface, or is null.
    if (!(o instanceof MyType)) {
        return false;
    }

    MyType lhs = (MyType) o; // lhs means "left hand side"

            // Primitive fields
    return     booleanField == lhs.booleanField
            && byteField    == lhs.byteField
            && charField    == lhs.charField
            && shortField   == lhs.shortField
            && intField     == lhs.intField
            && longField    == lhs.longField
            && floatField   == lhs.floatField
            && doubleField  == lhs.doubleField

            // Arrays

            && Arrays.equals(arrayField, lhs.arrayField)

            // Objects

            && referenceField.equals(lhs.referenceField)
            && (nullableReferenceField == null
                        ? lhs.nullableReferenceField == null
                        : nullableReferenceField.equals(lhs.nullableReferenceField));
}

1
Документация Android теперь не включает в себя вышеупомянутый код, поэтому здесь есть кэшированная версия Wayback Machine - Документация Android (7 февраля 2015 г.)
— Кристофер Ручински

17

Сначала убедитесь, что equals реализован правильно. Из статьи IBM DeveloperWorks :

  • Симметрия: Для двух ссылок, a и b, a.equals (b) тогда и только тогда, когда b.equals (a)
  • Рефлексивность: Для всех ненулевых ссылок, a.equals (a)
  • Транзитивность: если a.equals (b) и b.equals (c), то a.equals (c)

Затем убедитесь, что их отношение с hashCode соответствует контакту (из той же статьи):

  • Согласованность с hashCode (): два равных объекта должны иметь одинаковое значение hashCode ()

Наконец, хорошая хеш-функция должна стремиться приблизиться к идеальной хеш-функции .


11

about8.blogspot.com, вы сказали

если equals () возвращает true для двух объектов, то hashCode () должна возвращать одно и то же значение. Если equals () возвращает false, то hashCode () должен возвращать разные значения

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

Если A равно B, то A.hashcode должен быть равен B.hascode

но

если A.hashcode равен B.hascode, это не означает, что A должен быть равен B


3
Если это (A != B) and (A.hashcode() == B.hashcode())то, что мы называем коллизией хеш-функции. Это потому, что кодомен хеш-функции всегда конечен, а домен - нет. Чем больше кодомен, тем реже должно происходить столкновение. Хорошие хеш-функции должны возвращать разные хеш-функции для разных объектов с максимально возможной вероятностью, учитывая конкретный размер кодомена. Это редко может быть полностью гарантировано, хотя.
— Кшиштоф Яблоньский

Это должен быть просто комментарий к вышеупомянутому посту Грею. Хорошая информация, но она на самом деле не отвечает на вопрос
— Кристофер Ручински

Хорошие комментарии, но будьте осторожны с использованием термина «разные объекты» ... потому что equals () и, следовательно, реализация hashCode () не обязательно относятся к разным объектам в контексте ОО, но обычно больше относятся к их представлениям модели предметной области (например, два люди могут считаться одинаковыми, если они совместно используют код страны и идентификатор страны - хотя это могут быть два разных «объекта» в JVM - они считаются «равными» и имеют заданный хэш-код) ...
— Даррелл Тиг,

7

Если вы используете eclipse, вы можете сгенерировать equals()и hashCode()использовать:

Source -> Generate hashCode () и equals ().

Используя эту функцию, вы можете решить, какие поля вы хотите использовать для вычисления равенства и хеш-кода, и Eclipse генерирует соответствующие методы.


7

Там хорошая реализация из Эффективное Java «s hashcode()и equals()логики в Apache Commons Lang . Оформить заказ HashCodeBuilder и EqualsBuilder .


1
Недостатком этого API является то, что вы платите стоимость создания объекта каждый раз, когда вызываете метод equals и hashcode (если только ваш объект не является неизменяемым и вы предварительно вычисляете хеш), что может быть много в некоторых случаях.
— Джеймс МакМэхон

до недавнего времени это был мой любимый подход. Я столкнулся с StackOverFlowError при использовании критериев для ассоциации SharedKey OneToOne. Более того, Objectsкласс предоставляет hash(Object ..args)& equals()методы из Java7 на. Они рекомендуются для любых приложений, использующих jdk 1.7+
— Diablo

@Diablo Полагаю, вашей проблемой был цикл в графе объектов, и вам не повезло с большинством реализаций, так как вам нужно игнорировать какую-то ссылку или разорвать цикл (мандатировать IdentityHashMap). FWIW Я использую хэш-код на основе идентификатора и равняется для всех сущностей.
— Maaartinus

6

Просто быстрое примечание для завершения другого более подробного ответа (в терминах кода):

Если я рассмотрю вопрос « как сделать-я-создать-хэш-таблицу-в-java» и, в частности, статью «Часто задаваемые вопросы по jGuru» , я считаю, что существуют другие критерии, по которым можно судить о хэш-коде:

  • синхронизация (поддерживает ли алгоритм одновременный доступ или нет)?
  • отказоустойчивая итерация (обнаруживает ли алгоритм коллекцию, которая изменяется во время итерации)
  • нулевое значение (поддерживает ли хеш-код пустое значение в коллекции)

4

Если я правильно понимаю ваш вопрос, у вас есть собственный класс коллекции (т. Е. Новый класс, который выходит из интерфейса Collection), и вы хотите реализовать метод hashCode ().

Если ваш класс коллекции расширяет AbstractList, вам не нужно об этом беспокоиться, уже есть реализация equals () и hashCode (), которая работает путем перебора всех объектов и добавления их hashCodes () вместе.

   public int hashCode() {
      int hashCode = 1;
      Iterator i = iterator();
      while (i.hasNext()) {
        Object obj = i.next();
        hashCode = 31*hashCode + (obj==null ? 0 : obj.hashCode());
      }
  return hashCode;
   }

Теперь, если то, что вы хотите, является наилучшим способом вычисления хеш-кода для определенного класса, я обычно использую оператор ^ (побитовый исключающий или) для обработки всех полей, которые я использую в методе equals:

public int hashCode(){
   return intMember ^ (stringField != null ? stringField.hashCode() : 0);
}

2

@ about8: там довольно серьезная ошибка.

Zam obj1 = new Zam("foo", "bar", "baz");
Zam obj2 = new Zam("fo", "obar", "baz");

тот же хэш-код

Вы, вероятно, хотите что-то вроде

public int hashCode() {
    return (getFoo().hashCode() + getBar().hashCode()).toString().hashCode();

(Вы можете получить hashCode непосредственно из int в Java в эти дни? Я думаю, что это делает автокастинг ... если это так, пропустите toString, это уродливо.)


3
ошибка в длинном ответе about8.blogspot.com - получение хеш-кода из конкатенации строк оставляет вас с хеш-функцией, одинаковой для любой комбинации строк, которые складываются в одну и ту же строку.
— SquareCog

1
Так что это мета-обсуждение и вообще не имеет отношения к вопросу? ;-)
— Huppie

1
Это исправление предложенного ответа, которое имеет довольно существенный недостаток.
— SquareCog

Это очень ограниченная реализация
— Кристофер Ручински

Ваша реализация избегает проблемы и вводит другую; Обмен fooи barприводит к тому же hashCode. Ваш toStringAFAIK не компилируется, и если он это делает, то он ужасно неэффективен. Нечто подобное 109 * getFoo().hashCode() + 57 * getBar().hashCode()быстрее, проще и не вызывает ненужных столкновений.
— Maaartinus

2

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


2

Используйте методы отражения в Apache Commons EqualsBuilder и HashCodeBuilder .


1
Если вы собираетесь использовать это, имейте в виду, что отражение стоит дорого. Честно говоря, я бы не стал использовать это ни для чего, кроме как выбрасывать код.
— Джеймс МакМэхон

2

Я использую крошечную оболочку, Arrays.deepHashCode(...)потому что она правильно обрабатывает массивы, предоставленные в качестве параметров

public static int hash(final Object... objects) {
    return Arrays.deepHashCode(objects);
}

1

Любой метод хэширования, который равномерно распределяет значение хеша по возможному диапазону, является хорошей реализацией. См эффективной Java ( http://books.google.com.au/books?id=ZZOiqZQIbRMC&dq=effective+java&pg=PP1&ots=UZMZ2siN25&sig=kR0n73DHJOn-D77qGj0wOxAxiZw&hl=en&sa=X&oi=book_result&resnum=1&ct=result ), есть хороший наконечник там для реализации хэш-кода (пункт 9, я думаю ...).


1

Я предпочитаю использовать служебные методы из библиотеки Google Collections lib из класса Objects, которые помогают мне поддерживать мой код в чистоте. Очень часто equalsи hashcodeметоды делаются из шаблона IDE, поэтому они не чисты для чтения.


1

Вот еще одна демонстрация подхода JDK 1.7+ с учетом логики суперкласса. Я считаю это довольно удобным с учетом класса Object hashCode (), чистой зависимости JDK и без дополнительной ручной работы. пожалуйста, обратите вниманиеObjects.hash() что допускается нулевое значение.

Я не включил никакой equals()реализации, но в действительности это вам, конечно, понадобится.

import java.util.Objects;

public class Demo {

    public static class A {

        private final String param1;

        public A(final String param1) {
            this.param1 = param1;
        }

        @Override
        public int hashCode() {
            return Objects.hash(
                super.hashCode(),
                this.param1);
        }

    }

    public static class B extends A {

        private final String param2;
        private final String param3;

        public B(
            final String param1,
            final String param2,
            final String param3) {

            super(param1);
            this.param2 = param2;
            this.param3 = param3;
        }

        @Override
        public final int hashCode() {
            return Objects.hash(
                super.hashCode(),
                this.param2,
                this.param3);
        }
    }

    public static void main(String [] args) {

        A a = new A("A");
        B b = new B("A", "B", "C");

        System.out.println("A: " + a.hashCode());
        System.out.println("B: " + b.hashCode());
    }

}

1

Стандартная реализация слаба, и ее использование приводит к ненужным конфликтам. Представь себе

class ListPair {
    List<Integer> first;
    List<Integer> second;

    ListPair(List<Integer> first, List<Integer> second) {
        this.first = first;
        this.second = second;
    }

    public int hashCode() {
        return Objects.hashCode(first, second);
    }

    ...
}

Сейчас,

new ListPair(List.of(a), List.of(b, c))

и

new ListPair(List.of(b), List.of(a, c))

имеют то же значение hashCode, 31*(a+b) + cчто и множитель, используемый дляList.hashCode получает здесь повторно. Очевидно, что столкновения неизбежны, но создание ненужных столкновений просто ... ненужно.

Там нет ничего существенно умного в использовании 31. Множитель должен быть нечетным, чтобы избежать потери информации (любой четный множитель теряет по меньшей мере самый старший бит, кратные четыре теряют два и т. Д.). Любой нечетный множитель можно использовать. Маленькие множители могут привести к более быстрым вычислениям (JIT может использовать сдвиги и дополнения), но, учитывая, что умножение имеет задержку в три цикла на современных Intel / AMD, это вряд ли имеет значение. Маленькие множители также приводят к большему столкновению для небольших входов, что иногда может быть проблемой.

Использовать простое число бессмысленно, поскольку простые числа не имеют смысла в кольце Z / (2 ** 32).

Поэтому я бы рекомендовал использовать случайно выбранное большое нечетное число (не стесняйтесь брать простое число). Поскольку процессоры i86 / amd64 могут использовать более короткую инструкцию для подстановки операндов в один байт со знаком, то для множителей, подобных 109, есть небольшое преимущество в скорости. Для минимизации коллизий возьмите что-то вроде 0x58a54cf5.

Использование разных множителей в разных местах полезно, но, вероятно, недостаточно для оправдания дополнительной работы.


0

При объединении хеш-значений я обычно использую метод объединения, который используется в библиотеке boost c ++, а именно:

seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);

Это делает довольно хорошую работу по обеспечению равномерного распределения. Для некоторого обсуждения того, как работает эта формула, смотрите сообщение StackOverflow: Магическое число в boost :: hash_combine

Хорошее обсуждение различных хеш-функций: http://burtleburtle.net/bob/hash/doobs.html


1
Это вопрос о Java, а не о C ++.
— 17

-1

Для простого класса часто проще всего реализовать hashCode () на основе полей класса, которые проверяются реализацией equals ().

public class Zam {
    private String foo;
    private String bar;
    private String somethingElse;

    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (obj == null) {
            return false;
        }

        if (getClass() != obj.getClass()) {
            return false;
        }

        Zam otherObj = (Zam)obj;

        if ((getFoo() == null && otherObj.getFoo() == null) || (getFoo() != null && getFoo().equals(otherObj.getFoo()))) {
            if ((getBar() == null && otherObj. getBar() == null) || (getBar() != null && getBar().equals(otherObj. getBar()))) {
                return true;
            }
        }

        return false;
    }

    public int hashCode() {
        return (getFoo() + getBar()).hashCode();
    }

    public String getFoo() {
        return foo;
    }

    public String getBar() {
        return bar;
    }
}

Наиболее важным является сохранение согласованности между hashCode () и equals (): если equals () возвращает true для двух объектов, то hashCode () должна возвращать одно и то же значение. Если equals () возвращает false, то hashCode () должен возвращать разные значения.


1
Вроде SquareCog уже заметили. Если хэш - код генерируется один раз из конкатенации двух строк чрезвычайно легко генерировать массу столкновений: ("abc"+""=="ab"+"c"=="a"+"bc"==""+"abc"). Это серьезный недостаток. Было бы лучше оценить хеш-код для обоих полей, а затем рассчитать их линейную комбинацию (предпочтительно с использованием простых чисел в качестве коэффициентов).
— Кшиштоф Яблонский

@ KrzysztofJabłoński Право. Кроме того, обмен fooи barпроизводит ненужные столкновения тоже.
— Maaartinus
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.