Найкраща реалізація методу hashCode для колекції


299

Як ми можемо прийняти рішення про найкращу реалізацію hashCode()методу для колекції (якщо припустити, що метод рівних був замінений правильно)?


2
з Java 7+, я думаю, Objects.hashCode(collection)має бути ідеальним рішенням!
Діабло

3
@Diablo Я взагалі не думаю, що відповідає на питання - цей метод просто повертається collection.hashCode()( hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/src/share/… )
cbreezier

Відповіді:


438

Найкраща реалізація? Це складне питання, оскільки це залежить від схеми використання.

А при майже у всіх випадках було запропоновано розумне здійснення добре в Джош Блох «s Ефективне Java в пункті 8 (друге видання). Найкраще це шукати там, бо автор там пояснює, чому підхід хороший.

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

  1. Створіть a 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, якщо f == null;
    • Якщо поле f - масив : перегляньте кожне поле як окремий елемент та обчисліть хеш-значення рекурсивно та комбінуйте значення, як описано далі.
  3. Поєднайте хеш-значення cз result:

    result = 37 * result + c
  4. Повернення result

Це повинно призвести до правильного розподілу значень хешу для більшості ситуацій використання.


45
Так, мені особливо цікаво, звідки походить число 37.
Кіп

17
Я використав пункт 8 книги "Ефективна Java" Джоша Блоха.
dmeister

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

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


140

Якщо ви задоволені ефективної реалізацією 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
Якщо у когось немає вагомих причин не використовувати їх, то їх обов'язково слід використовувати в будь-якому випадку. (Формулюючи його сильніше, як це слід сформулювати IMHO.) Застосовуються типові аргументи для використання стандартних реалізацій / бібліотек (найкраща практика, добре перевірена, менша схильність до помилок тощо).
Кіссакі

7
@ justin.hughey ви, схоже, розгублені. Єдиний випадок, який вам слід перекрити, hashCodeце якщо у вас є звичай equals, і саме для цього розроблені ці методи бібліотеки. Документація досить чітка щодо їх поведінки стосовно equals. Реалізація бібліотеки не вимагає позбавлення вас від усвідомлення характеристик правильної hashCodeреалізації - ці бібліотеки полегшують вам реалізацію такої відповідної реалізації для більшості випадків, коли equalsце перекрито.
бакар

6
Для будь-яких розробників Android, які дивляться на клас java.util.Objects, він був представлений лише в API 19, тому переконайтеся, що ви працюєте на KitKat або вище, інакше ви отримаєте NoClassDefFoundError.
Ендрю Келлі

3
Краща відповідь ІМО, хоча на прикладі я скоріше обрав би java.util.Objects.hash(...)метод JDK7, ніж com.google.common.base.Objects.hashCode(...)метод гуави . Я думаю, що більшість людей обрали б стандартну бібліотеку за додаткову залежність.
Malte Skoruppa

2
Якщо є два аргументи або більше, і якщо будь-який з них є масивом, результат може бути не таким, який ви очікуєте, оскільки hashCode()для масиву це просто його java.lang.System.identityHashCode(...).
starikoff

59

Краще скористатися функціоналом, який надає Eclipse, який робить досить непогану роботу, і ви можете вкласти свої зусилля та енергію в розробку ділової логіки.


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

1
+1 Погодьтеся з Quantum7, але я б сказав, що також дуже добре розуміти, що робить реалізація, створена Eclipse, і звідки вона отримує деталі її впровадження.
jwir3

15
Вибачте, але відповіді, що включають "функціональність, яку надає [деяка IDE]", насправді не є актуальною в контексті мови програмування взагалі. Є десятки IDE, і це не дає відповіді на питання ... а саме тому, що мова йде більше про алгоритмічне визначення і безпосередньо пов'язане з реалізацією рівних () - те, про що IDE нічого не знатиме.
Даррелл Teague

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;

}

EDIT

Як правило, коли ви переосмислюєте 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 - Документація на Android (07 лютого 2015 р.)
Крістофер Ручінський

17

Спершу переконайтеся, що рівність реалізована правильно. З статті 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())це, ми називаємо зіткнення хеш-функцій. Це тому, що кодомейн хеш-функції завжди обмежений, тоді як домен зазвичай - ні. Чим більший кодомен, тим рідше має відбуватися зіткнення. Хороші хеш-функції повинні повертати різні хеші для різних об'єктів з найбільшою можливою можливістю з огляду на конкретний розмір кодомена. Однак це рідко може бути повністю гарантованим.
Кшиштоф Яблонський

Це повинен бути просто коментарем до вищезгаданого допису до Грея. Хороша інформація, але насправді це не відповідає на питання
Крістофер Ручинський

Хороші коментарі, але будьте обережні щодо використання терміна "різні об'єкти" ... тому що дорівнює (), а значить, реалізація хеш-коду () не обов'язково стосується різних об'єктів в контексті ОО, але зазвичай більше стосується представлень їхньої доменної моделі (наприклад, двох людей можна вважати однаковими, якщо вони поділяють код країни та ідентифікатор країни - хоча це можуть бути два різних "об'єкти" в JVM - вони вважаються "рівними" і мають заданий хеш-код) ...
Даррелл Тіг

7

Якщо ви використовуєте затемнення, ви можете генерувати equals()та hashCode()використовувати:

Джерело -> Створити хеш-код () та дорівнює ().

За допомогою цієї функції ви можете визначити, які поля ви хочете використовувати для рівності та обчислення хеш-коду, а Eclipse генерує відповідні методи.


7

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


1
Мінусом цього API є те, що ви сплачуєте витрати на будівництво об'єкта щоразу, коли ви називаєте рівними та хеш-коди (якщо ваш об'єкт є непорушним і ви попередньо обчислите хеш), що може бути дуже багато в певних випадках.
Джеймс Макмахон

це був мій улюблений підхід, до недавнього часу. Я наткнувся на StackOverFlowError, використовуючи критерії асоціації SharedKey OneToOne. Більше того, Objectsклас надає hash(Object ..args)та equals()методи від Java7 далі. Вони рекомендовані для будь-яких додатків, які використовують jdk 1.7+
Diablo

@Diablo Я думаю, вашою проблемою був цикл в графіку об'єкта, і тоді вам не пощастило з більшою частиною реалізації, оскільки вам потрібно ігнорувати якусь посилання або порушити цикл (мандат IdentityHashMap). FWIW Я використовую хеш-код на основі id і дорівнює для всіх об'єктів.
maaartinus

6

Просто коротка примітка для заповнення іншої більш детальної відповіді (в терміні коду):

Якщо я розглядаю питання про те, як зробити-я-створити-хеш-таблицю в java, і, особливо, питання про jGuru FAQ , я вважаю, що деякі інші критерії, за якими можна судити хеш-код, такі:

  • синхронізація (підтримує альго одночасний доступ чи ні)?
  • невдала безпечна ітерація (чи алго виявляє колекцію, яка змінюється під час ітерації)
  • null value (чи підтримує хеш-код нульове значення в колекції)

4

Якщо я правильно розумію ваше запитання, у вас є власний клас колекції (тобто новий клас, який поширюється на інтерфейс колекції), і ви хочете реалізувати метод hashCode ().

Якщо ваш колекційний клас поширюється на AbstractList, вам не доведеться турбуватися про це, вже існує реалізація рівнянь () та хеш-коду (), яка працює за допомогою ітерації всіх об'єктів та додавання їх хеш-кодів () разом.

   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();

(чи можете ви отримати хеш-код прямо з 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

Я вважаю за краще використовувати корисні методи fromm Google Collections lib від класу Objects, що допомагає мені зберегти чистий код. Дуже часто equalsі hashcodeметоди робляться з шаблону IDE, тому їх не є чистими для читання.


1

Ось ще одна демонстрація підходу JDK 1.7+ з логікою суперкласу. Я вважаю це досить сприятливим для облікового запису hashCode () класу Object, чистою залежністю від 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 ++.
Дано

-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.