1. Обзор
Хэшинг является фундаментальной концепцией информатики.
В Java эффективные алгоритмы хэширования стоят за некоторыми из самых популярных коллекций, которые у нас есть, например, HashMap (для углубленного изучения HashMap , не стесняйтесь, чтобы проверить эта статья ) и HashSet .
В этой статье мы сосредоточимся на том, как хэш-код () работает, как он играет в коллекции и как реализовать его правильно.
Дальнейшее чтение:
Java равно () и хэш-код () Контракты
Создание равных () и хэш-кода () с Eclipse
Введение в проект Ломбок
2. Использование хэш-кода () в структурах данных
Простейшие операции по сборам могут быть неэффективными в определенных ситуациях.
Например, это вызывает линейный поиск, который является крайне неэффективным для списков огромных размеров:
Listwords = 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) { Mapusers = 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 .