Руководство по hashCode () в Java

1. Обзор

Хеширование является фундаментальной концепцией информатики.

В Java эффективные алгоритмы хеширования стоят за некоторыми из самых популярных коллекций, которые у нас есть - например, HashMap (для более глубокого изучения HashMap , не стесняйтесь проверять ссылку:/java-hashmap[эта статья]) и https://docs.oracle.com/javase/7/docs/api/java/util/HashSet .html[HashSet].

В этой статье мы сосредоточимся на том, как hashCode () работает, как он играет на коллекциях и как правильно его реализовать.

2. Использование hashCode () в структурах данных

Простейшие операции с коллекциями могут быть неэффективными в определенных ситуациях.

Например, это вызывает линейный поиск, который крайне неэффективен для списков огромных размеров:

List<String> words = Arrays.asList("Welcome", "to", "Baeldung");
if (words.contains("Baeldung")) {
    System.out.println("Baeldung is in the list");
}

Java предоставляет ряд структур данных, специально предназначенных для решения этой проблемы - например, несколько реализаций интерфейса Map представляют собой таблицы hash.

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

3. Понимание того, как hashCode () работает

Проще говоря, hashCode () возвращает целочисленное значение, сгенерированное алгоритмом хеширования.

Объекты, которые равны (согласно их equals () ), должны возвращать тот же хэш-код. Не требуется, чтобы разные объекты возвращали разные хеш-коды.

Генеральный контракт hashCode () гласит:

  • Всякий раз, когда он вызывается на один и тот же объект более одного раза в течение

При выполнении Java-приложения hashCode () должен последовательно возвращать одно и то же значение при условии, что никакая информация, используемая в сравнениях сравнения объекта, не изменяется. Это значение не должно оставаться согласованным при выполнении одного приложения другим исполнением того же приложения

  • Если два объекта равны по методу equals (Object) ,

затем вызов метода hashCode () для каждого из двух объектов должен давать одно и то же значение

  • Не обязательно, если два объекта неравны в соответствии с

equals(java.lang.Object) , затем вызывая метод hashCode для каждого из двух объектов должен давать разные целочисленные результаты. Однако разработчики должны знать, что получение разных целочисленных результатов для неравных объектов повышает производительность хеш-таблиц.

«Насколько это практически возможно, метод hashCode () , определенный классом Object , действительно возвращает разные целые числа для разных объектов. (Это обычно реализуется путем преобразования внутреннего адреса объекта в целое число, но этот метод реализации не требуется языком программирования JavaTM.) » __

4. Наивная hashCode () Реализация

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

Чтобы продемонстрировать это, мы собираемся определить пример класса User , который переопределяет реализацию метода по умолчанию:

public class User {

    private long id;
    private String name;
    private String email;

   //standard getters/setters/constructors

    @Override
    public int hashCode() {
        return 1;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if (this.getClass() != o.getClass()) return false;
        User user = (User) o;
        return id == user.id
          && (name.equals(user.name)
          && email.equals(user.email));
    }

   //getters and setters here
}

Класс User предоставляет пользовательские реализации для equals () и hashCode () , которые полностью соответствуют соответствующим контрактам. Более того, нет ничего незаконного в том, что hashCode () возвращает любое фиксированное значение.

Однако эта реализация снижает функциональность хэш-таблиц до нуля, поскольку каждый объект будет храниться в одном и том же контейнере. **

В этом контексте поиск в хеш-таблице выполняется линейно и не дает нам никакого реального преимущества - подробнее об этом в разделе 7.

5. Улучшение реализации hashCode ()

Давайте немного улучшим текущую реализацию hashCode () , включив все поля класса User , чтобы он мог давать разные результаты для неравных объектов:

@Override
public int hashCode() {
    return (int) id **  name.hashCode() **  email.hashCode();
}

Этот основной алгоритм хеширования определенно намного лучше предыдущего, поскольку он вычисляет хеш-код объекта, просто умножая хеш-коды полей name и email и id .

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

6. Стандартный hashCode () Реализации

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

Давайте посмотрим на «стандартную» реализацию, которая использует два простых числа, чтобы добавить еще больше уникальности к вычисляемым хеш-кодам:

@Override
public int hashCode() {
    int hash = 7;
    hash = 31 **  hash + (int) id;
    hash = 31 **  hash + (name == null ? 0 : name.hashCode());
    hash = 31 **  hash + (email == null ? 0 : email.hashCode());
    return hash;
}

Хотя важно понимать роли, которые играют методы hashCode () и equals () , нам не нужно каждый раз реализовывать их с нуля, поскольку большинство IDE могут генерировать пользовательские реализации hashCode () и equals () начиная с Java 7 мы получили служебный метод Objects.hash () для удобного хеширования:

Objects.hash(name, email)

IntelliJ IDEA создает следующую реализацию:

@Override
public int hashCode() {
    int result = (int) (id ^ (id >>> 32));
    result = 31 **  result + name.hashCode();
    result = 31 **  result + email.hashCode();
    return result;
}

И Eclipse производит это:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime **  result + ((email == null) ? 0 : email.hashCode());
    result = prime **  result + (int) (id ^ (id >>> 32));
    result = prime **  result + ((name == null) ? 0 : name.hashCode());
    return result;
}

В дополнение к вышеуказанным реализациям hashCode () на основе IDE также возможно автоматически генерировать эффективную реализацию, например, используя Lombok . В этом случае зависимость lombok-maven должна быть добавлена ​​в pom.xml :

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok-maven</artifactId>
    <version>1.16.18.0</version>
    <type>pom</type>
</dependency>

Теперь достаточно аннотировать класс User с помощью @ EqualsAndHashCode :

@EqualsAndHashCode
public class User {
   //fields and methods here
}

Аналогично, если мы хотим, чтобы Apache HashCodeBuilder class Commons Lang’а сгенерировал hashCode () реализация для нас, https://search.maven.org/classic/#search%7Cga%7C1%7Capache-commons-lang[commons-lang Зависимость Maven должна быть включена в файл pom:

<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.6</version>
</dependency>

И hashCode () может быть реализовано так:

public class User {
    public int hashCode() {
        return new HashCodeBuilder(17, 37).
        append(id).
        append(name).
        append(email).
        toHashCode();
    }
}

В общем, универсального рецепта, которого можно придерживаться при реализации hashCode () , не существует. Мы настоятельно рекомендуем прочитать Joshua Bloch’s Effective Java , где представлен список подробные рекомендации для реализации эффективных алгоритмов хеширования.

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

31 **  i == (i << 5) - i

7. Обработка хеш-коллизий

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

Эта ситуация широко известна как коллизия хешей, и для ее обработки существуют various методологии , каждая из которых имеет свои плюсы и минусы. HashMap в Java использует https://en.wikipedia.org/wiki/Hash table#Separate chaining with linked__lists[the отдельный метод сцепления]для обработки коллизий:

  • «Когда два или более объектов указывают на один и тот же сегмент, они просто сохраняются в связанном списке. В таком случае хеш-таблица представляет собой массив связанных списков, и каждый объект с таким же хешем добавляется в связанный список по индексу корзины в массиве. **

  • В худшем случае к нескольким сегментам будет привязан связанный список, а поиск объекта в списке будет выполняться линейно ** ».

Методологии коллизий хэшей в двух словах показывают, почему так важно эффективно реализовать hashCode ()

Java 8 принесла интересную enhancement для реализации HashMap - если размер сегмента превышает определенный порог, связанный список заменяется древовидной картой. Это позволяет достичь _O ( logn ) поиска вместо пессимистического O (n) _ .

8. Создание простого приложения

Чтобы проверить функциональность стандартной реализации hashCode () , давайте создадим простое Java-приложение, которое добавляет некоторые объекты User в HashMap и использует ссылку:/slf4j-with-log4j2-logback[SLF4J]для регистрации сообщений на консоли каждый время вызывается метод.

Вот точка входа примера приложения:

public class Application {

    public static void main(String[]args) {
        Map<User, User> users = new HashMap<>();
        User user1 = new User(1L, "John", "[email protected]");
        User user2 = new User(2L, "Jennifer", "[email protected]");
        User user3 = new User(3L, "Mary", "[email protected]");

        users.put(user1, user1);
        users.put(user2, user2);
        users.put(user3, user3);
        if (users.containsKey(user1)) {
            System.out.print("User found in the collection");
        }
    }
}

И это реализация hashCode () :

public class User {

   //...

    public int hashCode() {
        int hash = 7;
        hash = 31 **  hash + (int) id;
        hash = 31 **  hash + (name == null ? 0 : name.hashCode());
        hash = 31 **  hash + (email == null ? 0 : email.hashCode());
        logger.info("hashCode() called - Computed hash: " + hash);
        return hash;
    }
}

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

----[main]INFO com.baeldung.entities.User - hashCode() called - Computed hash: 1255477819[main]INFO com.baeldung.entities.User - hashCode() called - Computed hash: -282948472[main]INFO com.baeldung.entities.User - hashCode() called - Computed hash: -1540702691[main]INFO com.baeldung.entities.User - hashCode() called - Computed hash: 1255477819
User found in the collection
----

9. Заключение

Очевидно, что для создания эффективных реализаций hashCode () часто требуется сочетание нескольких математических понятий (то есть простых и произвольных чисел), логических и базовых математических операций.

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

Как всегда, все примеры кода, показанные в этой статье, доступны на GitHub over .