Посібник з hashCode () на Java

1. Огляд

Хешування - основне поняття інформатики.

В Java, ефективні алгоритми хешування стоять за деякі з найбільш популярних колекцій , які ми маємо в розпорядженні - такі , як HashMap (для погляду поглиблених в HashMap , НЕ соромтеся перевірити цю статтю) і HashSet.

У цій статті ми зупинимося на тому, як працює hashCode () , як він відтворюється у колекціях та як правильно його реалізувати.

2. Використання hashCode () у структурах даних

Найпростіші операції з колекціями можуть бути неефективними в певних ситуаціях.

Наприклад, це викликає лінійний пошук, що є дуже неефективним для списків величезних розмірів:

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

Java надає ряд структур даних для вирішення цієї проблеми - наприклад, кілька реалізацій інтерфейсу Map - це хеш-таблиці.

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

3. Розуміння того, як Hashcode () робота

Простіше кажучи, hashCode () повертає ціле значення, сформоване алгоритмом хешування.

Об'єкти, які рівні (відповідно до їх рівних () ), повинні повертати той самий хеш-код. Для різних об’єктів не потрібно повертати різні хеш-коди.

Загальний контракт hashCode () передбачає:

  • Кожного разу, коли він викликається для одного і того ж об'єкта більше одного разу під час виконання програми Java, hashCode () повинен послідовно повертати одне і те ж значення, за умови, що жодна інформація, яка використовується для порівняння рівних об'єкта, не змінюється. Це значення не повинно залишатися незмінним від одного виконання програми до іншого виконання тієї самої програми
  • Якщо два об'єкти рівні за методом equals (Object) , тоді виклик методу hashCode () для кожного з двох об'єктів повинен дати одне і те ж значення
  • Не потрібно, якщо два об'єкти нерівні за методом equals (java.lang.Object) , тоді виклик методу hashCode для кожного з двох об'єктів повинен давати різні цілі результати. Однак розробники повинні знати, що отримання чітких цілочисельних результатів для нерівних об'єктів покращує продуктивність хеш-таблиць

“Наскільки це практично можливо, метод hashCode (), визначений класом Object , повертає різні цілі числа для різних об’єктів. (Це зазвичай реалізується шляхом перетворення внутрішньої адреси об'єкта в ціле число, але ця техніка реалізації не потрібна мовою програмування JavaTM.) "

4. Наївні хеш - код () Реалізація

Насправді досить просто мати наївну реалізацію 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 () , включивши всі поля класу User, щоб він міг давати різні результати для нерівних об'єктів:

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

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

Загалом, можна сказати, що це розумна реалізація hashCode () , якщо ми підтримуємо відповідність реалізації equals () .

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

Хоча важливо розуміти ролі, які відіграють методи 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 :

 org.projectlombok lombok-maven 1.16.18.0 pom 

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

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

Подібним чином, якщо ми хочемо, щоб клас HashCodeBuilder Apache Commons Lang генерував для нас реалізацію hashCode () , залежність commons-lang Maven повинна бути включена у файл pom:

 commons-lang commons-lang 2.6 

А hashCode () можна реалізувати так:

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

Загалом, немає універсального рецепту, якого слід дотримуватися, коли справа стосується впровадження hashCode () . Ми настійно рекомендуємо прочитати "Ефективну Java" Джошуа Блоха, де наведено перелік ретельних вказівок щодо впровадження ефективних алгоритмів хешування.

Тут можна помітити, що всі ці реалізації використовують число 31 у певній формі - це тому, що 31 має приємну властивість - його множення може бути замінене побітовим зсувом, який швидший за стандартне множення:

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

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

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

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

“Коли два або більше об’єктів вказують на одне і те ж сегмент, вони просто зберігаються у зв’язаному списку. У такому випадку хеш-таблиця є масивом зв’язаних списків, і кожен об’єкт з однаковим хешем додається до зв’язаного списку за індексом сегмента в масиві.

У гіршому випадку декілька сегментів мали б пов’язаний зв’язаний список, а пошук об’єкта у списку виконувався б лінійно ”.

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

Java 8 внесла цікаве вдосконалення в реалізацію HashMap - якщо розмір сегмента перевищує певний поріг, пов'язаний список замінюється картою дерева. Це дозволяє досягти пошуку O ( logn ) , а не песимістичного O (n) .

8. Створення тривіального додатка

Щоб перевірити функціональність стандартної реалізації hashCode () , давайте створимо просту програму 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"); } } } 

А це реалізація 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; } }

The only detail worth stressing here is that each time an object is stored in the hash map and checked with the containsKey() method, hashCode() is invoked and the computed hash code is printed out to the console:

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

It's clear that producing efficient hashCode() implementations often requires a mixture of a few mathematical concepts, (i.e. prime and arbitrary numbers), logical and basic mathematical operations.

Regardless, it's entirely possible to implement hashCode() effectively without resorting to these techniques at all, as long as we make sure the hashing algorithm produces different hash codes for unequal objects and is consistent with the implementation of equals().

Як завжди, усі приклади коду, показані в цій статті, доступні на GitHub.