equals()
і hashCode()
. Ми зустрічаємося з ними не вперше: на початку курсу JavaRush була невелика лекція про equals()
- прочитай її, якщо призабув або не зустрічав раніше. На сьогоднішньому ж занятті поговоримо про ці поняття докладно - повір, поговорити є про що! І перед тим, як переходити до нового, давай освіжити в пам'яті те, що вже проходабо :) Як ти пам'ятаєш, звичайне порівняння двох об'єктів через оператор ” – погана ідея, бо ” ==
порівнює ==
посилання. Ось наш приклад із машинами з нещодавньої лекції:
public class Car {
String model;
int maxSpeed;
public static void main(String[] args) {
Car car1 = new Car();
car1.model = "Ferrari";
car1.maxSpeed = 300;
Car car2 = new Car();
car2.model = "Ferrari";
car2.maxSpeed = 300;
System.out.println(car1 == car2);
}
}
Виведення в консоль:
false
Здавалося б, ми створабо два ідентичні об'єкти класу Car
: всі поля у двох машин однакові, але результат порівняння все одно false. Причина нам вже відома: посилання car1
і car2
вказують на різні адресаи в пам'яті, тому вони не є рівними. Ми ж хочемо порівняти два об'єкти, а не два посилання. Найкраще рішення для порівняння об'єктів - метод equals()
.
Метод equals()
Можливо, ти пам'ятаєш, що цей метод ми не створюємо з нуля, а перевизначаємо — адже методequals()
визначено у класі Object
. Однак у звичайному вигляді користі від нього мало:
public boolean equals(Object obj) {
return (this == obj);
}
Ось так метод equals()
визначений у класі Object
. Те саме порівняння посилань. Для чого його зробабо таким? Ну а звідки творцям мови знати, які об'єкти у твоїй програмі вважати рівними, а які ні? :) У цьому полягає основна ідея методу equals()
- творець класу сам визначає характеристики, за якими перевіряється рівність об'єктів цього класу. Зробивши це, ти перевизначаєш метод equals()
у своєму класі. Якщо тобі не зовсім зрозумілий сенс сам визначаєш характеристики, давай розглянемо приклад. Ось простий клас людини Man
.
public class Man {
private String noseSize;
private String eyesColor;
private String haircut;
private boolean scars;
private int dnaCode;
public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
this.noseSize = noseSize;
this.eyesColor = eyesColor;
this.haircut = haircut;
this.scars = scars;
this.dnaCode = dnaCode;
}
//Геттери, сетери і т.д.
}
Припустимо, ми пишемо програму, яка має визначати, чи є дві людини родичами-близнюками, чи це просто двійники. У нас є п'ять характеристик: розмір носа, колір очей, зачіска, наявність шрамів та результати біологічного тесту ДНК (для простоти у вигляді кодового числа). Як ти вважаєш, які з цих характеристик дозволять нашій програмі визначити родичів-близнюків? Зрозуміло, гарантію може дати лише біологічний тест. У двох людей може бути однаковий колір очей, зачіска, ніс, і навіть шрами — людей у світі багато, і уникнути збігів неможливо. Нам же потрібний надійний механізм: лише результат ДНК-тесту дозволяє зробити точний висновок. Що ж це означає для нашого методу equals()
? Нам потрібно його перевизначити у класіMan
з урахуванням вимог нашої програми. Метод повинен порівнювати поле int dnaCode
двох об'єктів, і якщо вони рівні, то й об'єкти рівні.
@Override
public boolean equals(Object o) {
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Невже так просто? Не зовсім. Ми пропустабо дещо. В даному випадку для наших об'єктів ми визначабо лише одне «значуще» поле, за яким встановлюється їхня рівність — dnaCode
. А тепер уяви, що таких «значних» полів у нас було б не 1, а 50. І якщо всі 50 полів у двох об'єктів рівні, то об'єкти рівні. Таке також може бути. Головна проблема в тому, що обчислення рівності 50 полів - витратний за часом та ресурсами процес. Тепер уяви, що крім класу Man
у нас є клас Woman
з такими ж полями, як і в Man
. І якщо твоїми класами користуватиметься інший програміст, він запитто може написати у своїй програмі щось на кшталт:
public static void main(String[] args) {
Man man = new Man(........); //Купа параметрів у конструкторі
Woman woman = new Woman(.........);//така ж купа параметрів.
System.out.println(man.equals(woman));
}
Перевіряти значення полів у разі безглуздо: ми бачимо, що маємо об'єкти двох різних класів, і вони можуть бути рівні у принципі! Значить, у метод equals()
нам потрібно помістити перевірку — порівняння об'єктів двох однакових класів. Добре, що ми про це подумали!
@Override
public boolean equals(Object o) {
if (getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Але, може, ми забули щось ще? Хм… Як мінімум, треба було б перевірити, що ми не порівнюємо об'єкт сам із собою! Якщо посилання А і Б вказують на одну адресау в пам'яті, це один і той же об'єкт, і нам теж не треба витрачати час і порівнювати 50 полів.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Крім того, не завадить додати перевірку на null
: ніякий об'єкт не може дорівнювати null
, і в такому випадку немає сенсу в додаткових перевірках. З урахуванням всього цього, наш метод equals()
для класу Man
виглядатиме так:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Ми проводимо всі початкові перевірки, про які сказали вище. Якщо в результаті виявилося, що:
- ми порівнюємо два об'єкти одного класу
- це не один і той самий об'єкт
- ми порівнюємо наш об'єкт не з
null
dnaCode
двох об'єктів. Перевизначаючи метод equals()
, обов'язково дотримуйся цих вимог:
-
Рефлексивність.
Будь-який об'єкт має бути
equals()
самому собі.
Ми вже врахували цю вимогу. У нашому методі вказано:if (this == o) return true;
-
Симетричність.
Якщо
a.equals(b) == true
, то йb.equals(a)
має повертатиtrue
.
Цій вимогі наш метод теж відповідає. -
Транзитивність.
Якщо два об'єкти дорівнюють якомусь третьому об'єкту, значить, вони повинні дорівнювати один і одному.
Якщо ,a.equals(b) == true
значитьa.equals(c) == true
перевіркаb.equals(c)
теж повинна повертати true. -
Постійність.
Результати роботи
equals()
повинні змінюватися тільки при зміні полів, що входять до нього. Якщо дані двох об'єктів не змінювалися, результати перевіркиequals()
повинні бути завжди однаковими. -
Нерівність с
null
.Це не просто набір якихось «корисних рекомендацій», а саме жорсткий
a.equals(null)
контракт методів , прописаний у документації Oracle
Метод hashCode()
Тепер поговоримо про методhashCode()
. Навіщо він потрібен? Рівно для тієї ж мети порівняння об'єктів. Але ж у нас уже є equals()
! Навіщо ще один метод? Відповідь проста: підвищення продуктивності. Хеш-функція, яка представлена Java методом hashCode()
, повертає числове значення фіксованої довжини для будь-якого об'єкта. У випадку Java метод hashCode()
повертає для будь-якого об'єкта 32-бітове число типу int
. Порівняти два числа між собою набагато швидше, ніж порівняти два об'єкти методом equals()
, особливо якщо в ньому використовується багато полів. Якщо в нашій програмі будуть порівнюватися об'єкти, набагато простіше зробити це за хеш-кодом, і тільки якщо вони рівні по hashCode()
- переходити до порівнянняequals()
. Таким чином, до речі, працюють засновані на хеш структури даних - наприклад, відома тобі HashMap
! Метод hashCode()
, як і equals()
, перевизначається самим розробником. І так само, як для equals()
, для методу hashCode()
є офіційні вимоги, прописані в документації Oracle:
-
Якщо два об'єкти рівні (тобто метод
equals()
повертає true), вони мають бути однаковий хеш-код.Інакше наші методи будуть позбавлені сенсу. Перевірка по
hashCode()
, Як ми й сказали, повинна йти першою для підвищення швидкодії. Якщо хеш-коди будуть різними, перевірка поверне false, хоча об'єкти насправді рівні (згідно з нашим визначенням у методіequals()
). -
Якщо метод
hashCode()
викликається кілька разів на тому самому об'єкті, щоразу він повинен повертати те саме число. -
Правило 1 не працює у зворотний бік. Одинаковий хеш-код може бути у двох різних об'єктів.
hashCode()
повертає int
. int
- Це 32-бітне число. Він має обмежену кількість значень — від -2,147,483,648 до +2,147,483,647. Іншими словами, всього існує трохи більше 4 мільярдів варіантів числа int
. Тепер уяви, що ти створюєш програму для зберігання даних про всіх людей, що живуть на Землі. Кожній людині відповідатиме свій об'єкт класу Man
. На землі мешкає ~7.5 мільярда людей. Іншими словами, який би хороший алгоритм перетворення об'єктівMan
до числа ми не написали, нам просто не вистачить чисел. У нас лише 4,5 мільярда варіантів, а людей набагато більше. Отже, хоч би як ми намагалися, для якихось різних людей хеш-коди будуть однаковими. Така ситуація (збіг хеш-кодів у двох різних об'єктів) називається колізією. Одне із завдань програміста при перевизначенні методу hashCode()
— скоротити потенційну кількість колізій наскільки це можливо. Яким же буде виглядати наш метод hashCode()
для класу Man
з урахуванням усіх цих правил? Ось так:
@Override
public int hashCode() {
return dnaCode;
}
Здивований? :) Несподівано, але якщо ти подивишся на вимоги, побачиш, що ми дотримуємося всіх. Об'єкти, для яких наш equals()
повертає true, будуть рівними і по hashCode()
. Якщо два наші об'єкти Man
будуть рівні equals
(тобто у них однаковий dnaCode
), наш метод поверне однакове число. Розглянемо приклад складніше. Допустимо, наша програма має відбирати елітні автомобілі для клієнтів-колекціонерів. Колекціонування – штука складна, і в ній багато особливостей. Автомобіль 1963 року випуску може коштувати у 100 разів дорожче, ніж той самий автомобіль 1964 року. Червоний автомобіль 1970 може коштувати в 100 разів дорожче, ніж синій автомобіль тієї ж марки того ж року. У першому випадку, з класомMan
, ми відкинули більшість полів (тобто характеристик людини) як незначні і порівняння використовували лише поле dnaCode
. Тут ми працюємо з дуже своєрідною сферою, і незначних деталей бути не може! Ось наш клас LuxuryAuto
:
public class LuxuryAuto {
private String model;
private int manufactureYear;
private int dollarPrice;
public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
this.model = model;
this.manufactureYear = manufactureYear;
this.dollarPrice = dollarPrice;
}
//...гетери, сетери і т.д.
}
Тут при порівнянні ми маємо враховувати всі поля. Будь-яка помилка може коштувати сотні тисяч доларів для клієнта, тому краще перестрахуватися:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LuxuryAuto that = (LuxuryAuto) o;
if (manufactureYear != that.manufactureYear) return false;
if (dollarPrice != that.dollarPrice) return false;
return model.equals(that.model);
}
У нашому методі equals()
ми не забули всіх перевірок, про які говорабо раніше. Але тепер ми порівнюємо кожне із трьох полів наших об'єктів. У цій програмі рівність має бути абсолютною, по кожному полю. А що ж з hashCode
?
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = result + manufactureYear;
result = result + dollarPrice;
return result;
}
Поле model
у нашому класі – рядок. Це зручно: у класі String
метод hashCode()
уже перевизначено. Ми обчислюємо хеш-код поля model
, а до нього додаємо суму двох інших числових полів. Java має одну невелику хитрість, яка використовується для скорочення кількості колізій: при обчисленні хеш-коду множити проміжний результат на непарне просте число. Найчастіше використовується число 29 або 31. Ми зараз не заглиблюватимемося в математичні тонкощі, але на майбутнє запам'ятай, що множення проміжних результатів на досить велике непарне число допомагає «розмазати» результати хеш-функції і отримати в результаті менше об'єктів з однаковим хешкодом. Для нашого методу hashCode()
в LuxuryAuto це буде виглядати так:
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
Докладніше про всі тонкощі цього механізму можна прочитати в цьому пості на StackOverflow , а також у Джошуа Блоха в книзі " Effective Java ". Насамкінець ще один важливий момент, про який варто сказати. Щоразу при перевизначенні equals()
і hashCode()
ми вибирали певні поля об'єкта, які в цих методах враховувалися. Але чи можемо ми враховувати різні поля в equals()
і hashCode()
? Технічно, можемо. Але це погана ідея, і ось чому:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LuxuryAuto that = (LuxuryAuto) o;
if (manufactureYear != that.manufactureYear) return false;
return dollarPrice == that.dollarPrice;
}
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
Ось наші методи equals()
для hashCode()
класу LuxuryAuto. Метод hashCode()
залишився без змін, та якщо з методу equals()
ми прибрали поле model
. Тепер модель - не характеристика для порівняння двох об'єктів equals()
. Але при розрахунку хеш-коду вона, як і раніше, враховується. Що ми отримаємо в результаті? Давай створимо два автомобілі та перевіримо!
public class Main {
public static void main(String[] args) {
LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);
System.out.println("Ці два об'єкти рівні один одному?");
System.out.println(ferrariGTO.equals(ferrariSpider));
System.out.println("Які у них хеш-коди?");
System.out.println(ferrariGTO.hashCode());
System.out.println(ferrariSpider.hashCode());
}
}
Эти два об'єкта равны друг другу?
true
Какие у них хэш-коды?
-1372326051
1668702472
Помилка! Використовуючи різні поля для equals()
та hashCode()
ми порушабо встановлений для них контракт! У двох рівних equals()
об'єктів повинен бути однаковий хеш-код. Ми ж здобули для них різні значення. Подібні помилки можуть призвести до неймовірних наслідків, особливо при роботі з колекціями, що використовують хеш. Тому при перевизначенні equals()
і hashCode()
правильно буде використовувати одні й самі поля. Лекція вийшла чималенькою, але сьогодні ти дізнався багато нового! :) Саме час повернутися до вирішення завдань!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ