Лучшая реализация метода 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.