Рубрики
Без рубрики

Руководство по хэш-коду () на Java

Узнайте, как работает хэш-код и как его правильно реализовать.

Автор оригинала: baeldung.

1. Обзор

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

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

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

Дальнейшее чтение:

Java равно () и хэш-код () Контракты

Создание равных () и хэш-кода () с Eclipse

Введение в проект Ломбок

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

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

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

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

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

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

3. Понимание того, как работает хэш-код ()

Проще говоря, хэш-код () возвращает значение integer, генерируемое алгоритмом хэширования.

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

Генеральный контракт хэш-код () Государств:

  • Всякий раз, когда он вызывается на одном и том же объекте более одного раза во время выполнения java-приложения, хэш-код () должны последовательно возвращать одно и то же значение, при условии, что никакая информация, используемая в равных сравнениях на объекте, не изменяется. Это значение не должно оставаться согласованным от одного выполнения приложения к другому исполнению одного и того же приложения
  • Если два объекта равны в соответствии с равны (объект) метод, а затем вызов хэш-код () метод на каждом из двух объектов должен производить одинаковое значение
  • Не требуется, чтобы, если два объекта являются неравными в соответствии с равны (java.lang.Object) метод, а затем вызов хэш-код метод на каждом из двух объектов должен дать различные интегративные результаты. Однако разработчики должны знать, что создание различных интегративных результатов для неравных объектов повышает производительность хэш-таблиц

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

4. Наивный хэш-код () Реализация

Это на самом деле довольно просто иметь наивный хэш-код () полностью придерживается вышеупомянутого контракта.

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

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
}

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

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

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

5. Улучшение хэш-кода () Реализация

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

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

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

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

6. Стандартный хэш-код () Реализации

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

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

@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;
}

Хотя очень важно понимать роли, которые хэш-код () и равны () методы игры, мы не должны осуществлять их с нуля каждый раз, так как большинство IDEs может генерировать пользовательские хэш-код () и равны () реализации и с Java 7, мы получили Объекты.хэш () утилита для удобного хэширования:

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;
}

И Затмение производит этот:

@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;
}

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


    org.projectlombok
    lombok-maven
    1.16.18.0
    pom

Теперь достаточно аннотировать Пользователь класс с @EqualsAndHashCode :

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

Точно так же, если мы хотим Апач Викисклад Ланг HashCodeBuilder класс для создания хэш-код () реализации для нас, общий ланг Зависимость Maven должна быть включена в файл pom:


    commons-lang
    commons-lang
    2.6

И хэш-код () могут быть реализованы так:

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

В общем, нет универсального рецепта придерживаться, когда дело доходит до реализации хэш-код () . Мы настоятельно рекомендуем читать Эффективный Java- Джошуа , который предоставляет список тщательные руководящие принципы

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

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

7. Обработка хэш-столкновений

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

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

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

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

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

Java 8 принес интересную повышение HashMap реализация – Если размер ведра выходит за пределы определенного порога, связанный список заменяется картой дерева. Это позволяет достичь O ( logn ) смотреть вверх, а не пессимистические О(н) .

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

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

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

public class Application {

    public static void main(String[] args) {
        Map 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");
        }
    }
}

И это хэш-код () реализация:

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;
    }
}

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

[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. Заключение

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

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

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