У цій статті ми розглянемо два важливі методи класу Object у Java - equals() і hashCode(), з’ясуємо навіщо вони потрібні і розповімо як їх застосовувати на конкретних прикладах.
Навіщо потрібні equals() і hashCode() та в чому полягає їхня особливість?
У програмуванні нерідко виникають ситуації, коли потрібно порівнювати об’єкти. Перше, що спадає на думку, коли хочеться щось порівняти, це перевірити умову написавши символ “==”. Здавалося б, це має вирішити всі наші проблеми, однак це не так. Символ “==” є сенс застосовувати лише з примітивами. Стосовно об’єктів, “==” в Java порівнює лише посилання, тобто перевіряє чи дорівнює об’єкт самому собі, а це не завжди може бути корисно. Для порівння об’єктів у класі Object існує метод equals(), саме він і має виконувати порівняння об’єктів. За замовчуванням цей метод виконує всередині себе операцію “==”, однак, ідея методу полягає в тому, що він має бути перевизначений для створюваних класів.
Яким же повинен бути перевизначений метод equals()?
Метод виглядає наступним чином: public boolean equals(Object o);
На вхід він приймає Object і повинен повернути true або false. Тепер розберемо покроково.
Як саме потрібно виконати порівняння двох об’єктів у в Java, щоб зробити його якісно та ефективно?
1) Порівнюючи два об’єкти варто розпочати з перевірки посилань. Адже, якщо ми порівнюємо об’єкт з самим собою, для чого нам проводити зайві перевірки і порівнювати поля об’єкту?
2) Перше ніж приступати до докладного порівняння слід виконати ще 2 перевірки, які також можуть відсіяти об’єкти які точно не дорівнюють один одному. Це прискорить роботу методу equals(). Ці перевірки допомагають зрозуміти, чи є об’єкт null та чи порівнюємо ми об’єкти однакових класів. Якщо ми від початку порівнюємо два null об’єкти, то перша перевірка поверне нам true, а отже на другому кроці об’єкт точно не має бути null.
3) Тепер, коли ми впевнилися, що обидва об’єкти належать до однакового класу і не дорівнюють null, можна привести об’єкт до потрібного типу й почати порівнювати поля.
Приклад
А ось і приклад перевизначеного методу, який в нас вийшов:
Виглядає непогано, але варто зазначити, що методи equals(), які викликаються для String та Integer вже перевизначені всередині цих класів. Якщо полем вашого класу є об’єкт іншого вашого класу, в ньому теж необхідно перевизначати метод equals(). Крім того, наведений метод цілком відповідає вимогам до нього, визначеним у документації Oracle, а саме:
1) Рефлексивність. Будь-який об’єкт, який не дорівнює null має бути equals() самому собі.
2) Симетричніть. Якщо a.equals(b) == true, то й b.equals(a) повинно повертати true.
3) Транзитивність. Якщо два об’єкти дорівнюють певному третьому об’єкту, то це означає, що вони мають бути рівні між собою.
4) Постійність. Результати роботи equals() мають змінюватися лише за умови зміни полів, що надходять в нього. Якщо дані двох об’єктів не змінювалися, результати перевірки на equals() повинні бути завжди однаковими.
5) Нерівність з null. Для будь-якого об’єкту, не рівному null перевірка a.equals(null) має поверати false
Але що робити, якщо нам всього лише потрібно переконатися в тому, що об’єкти не рівні між собою і зробити це дуже швидко?
Такий підхід застосовується, коли нам потрібно зберігати набір унікальних об’єктів, наприклад в такій структурі даних як HashMap. В цьому випадку метод equals() не дуже підходить, оскільки перевірка всіх полів об’єкту вимагає часу. Для цього використовується хеш-код об’єкту.
У Java хеш-код об’єкту можна отримати за допомогою методу hashCode(). Для будь-якого об’єкту цей метод повертає 32-бітне значення типу int. Хеш-код, який повертається, має задовольняти вимоги, прописані в документації Oracle, а саме:
1) Якщо два об’єкти рівні (тобто метод equals() повертає true), у них має бути однаковий хеш-код.
2) Якщо метод hashCode() викликати кілька разів на одному й тому ж об’єкті, щоразу він має повернути одне й те саме число.
3) Два різних об’єкти можуть мати однаковий хеш-код.
Останнє правило випливає з того, що хеш-код обмежений 32 бітами, а отже може приймати трохи більше 4 мільйярдів різних значень, при цьому кількість створюваних об’єктів не обмежена нічим, окрім пам’яті, яка доступна додатку.
Приклад
Розглянемо приклад, як може бути перевизначено метод hashCode() на прикладі нашого класу:
Множення на просте непарне число 31 використовується для зменшення числа колізій, тобто щоб різноманіття хеш-кодів, які розраховуються, було якомога більшим. Існує й більш доладний спосіб перевизначення хеш-коду. В класі Objects є метод hash(), на вхід якого потрібно надати всі поля класу. Цей метод робить все те саме, що зображено на попередньому малюнку:
Застосування хеш-коду дозволяє ефективно працювати з елементами в в HashMap. Про те, як саме досягається ця ефективність HashMap, які завдання вона вирішує та які проблеми можуть виникнути при роботі з нею, ми розповімо в одній з наступних публікацій.