— Тепер я розповім про не менш корисні методи equals(Object o) & hashCode().
Як ти вже, напевно, встиг запам'ятати, у Java при порівнянні посилальних змінних порівнюються не самі об'єкти, а посилання на об'єкти.
Код | Пояснення |
---|---|
|
i не дорівнює j Змінні вказують на різні об'єкти. Хоча об'єкти містять однакові дані; |
|
i дорівнює j Змінні містять посилання на один і той самий об'єкт. |
— Так, я це пам'ятаю.
— Є також стандартне вирішення цієї ситуації – метод equals.
Ціль методу equals – визначити, чи ідентичні об'єкти всередині, за допомогою порівняння внутрішнього змісту об'єктів.
— Як він це робить?
— Тут все аналогічно методуу toString().
У класу Object є своя реалізація методу equals, яка просто порівнює посилання:
public boolean equals(Object obj)
{
return (this == obj);
}
— З чим боролися, на те й напоролися.
— Не вішай голови. Тут все теж дуже хитро.
Цей метод створювався, щоб розробники перевизначали їх у своїх класах. Адже тільки розробник класу знає, які дані є важливими, що враховувати при порівнянні, а що – ні.
— А можна приклад такого методу?
— Звичайно. Припустимо, у нас є клас, який описує математичні дроби, тоді він виглядав би так (для ясності, я перекладу англійські назви українською мовою):
class Дріб
{
private int чисельник;
private int знаменник;
Дріб(int чисельник, int знаменник)
{
this.чисельник = чисельник;
this.знаменник = знаменник;
}public boolean equals(Object obj)
{
if (obj==null)
return false;
if (obj.getClass() != this.getClass() )
return false;
Дріб other = (Дріб) obj;
return this.чисельник* other.знаменник == this.знаменник * other.чисельник;
}
}
Приклад виклику: |
---|
Дріб one = new Дріб(2,3); Дріб two = new Дріб(4,6); System.out.println(one.equals(two)); |
Результат виклику буде true. дріб, що 2/3 дорівнює дробу 4/6 |
— Для більшої ясності я використав українські назви. Так можна робити лише у навчальних цілях.
Тепер розберемо приклад.
Ми перевизначили метод equals, і тепер для об'єктів класу Дріб у нього буде своя реалізація.
У цьому методі є кілька перевірок:
1) Якщо переданий для порівняння об'єкт – null, то об'єкти не дорівнюють один одному. Об'єкт, у якого викликали метод equals точно не null.
2) Перевірка порівняння класів. Якщо об'єкти різних класів, то ми не будемо намагатися їх порівняти, а одразу скажемо, що це різні об'єкти – return false.
3) З другого класу школи всі пам'ятають, що дріб 2/3 дорівнює дробу 4/6. А як це перевірити?
2/3 == 4/6 |
---|
Помножимо обидві частини на обидва дільники (6 і 3), отримаємо: |
6 * 2 == 4 * 3 |
12 == 12 |
Загальне правило: |
Якщо a / b == c / d То a * d == c * b |
— Тому в третій частині методу equals ми перетворюємо переданий об'єкт до типу Дріб і порівнюємо дроби.
— Зрозуміло. Якби ми просто порівнювали чисельник із чисельником і знаменник із знаменником, то дріб 2/3 не дорівнював би 4/6.
Тепер зрозуміло, що ти мав на увазі, коли казав, що лише розробник класу знає, як правильно його порівнювати.
— Так, але це лише частина справи. Ще є другий метод – hashCode()
— З методом equals все зрозуміло, а навіщо потрібен hashCode()?
— Метод hashCode потрібен для швидкого порівняння.
У методу equals є великий мінус – він занадто повільно працює. Наприклад, у тебе є множина(Set) з мільйона елементів, і нам потрібно перевірити, містить воно певний об'єкт чи ні. Як це зробити?
— Можна в циклі пройтися всіма елементами і порівняти потрібний об'єкт з кожним об'єктом множини. Поки не знайдемо потрібний.
— А якщо його там нема? Ми виконаємо мільйон порівнянь, щоб дізнатись, що там немає цього об'єкта? Чи не забагато?
— Так, навіть мені зрозуміло, що надто багато порівнянь. А що, є інший спосіб?
— Так, для цього і використовується hashCode().
Метод hashCode() для кожного об'єкта повертає певне число. Яке саме – це теж вирішує розробник класу, як і у випадку з методом equals.
Давай розглянемо ситуацію на прикладі:
Уяви, що маєш мільйон 10-тизначних чисел. Тоді як hashCode для кожного числа можна обрати залишок від його поділу на 100.
Приклад:
Число | Наш hashCode |
---|---|
1234567890 | 90 |
9876554321 | 21 |
9876554221 | 21 |
9886554121 | 21 |
— Так, із цим зрозуміло. І що нам робити з цим hashCode-числом?
— Замість того, щоб порівнювати числа, ми порівнюватимемо їхні hashCode. Так швидше.
І тільки якщо hashCode-и дорівнюють один одному, порівнювати вже за допомогою equals.
— Так швидше. Але нам все одно доведеться зробити мільйон порівнянь, тільки вже більш коротких чисел, а для тих чисел, чиї hashCode збігаються, знову викликати equals.
— Ні, можна обійтися набагато меншим числом.
Уяви, що наша множина зберігає числа, що згруповані за hashCode або відсортовані за hashCode (що рівносильно їх групуванню, тому що числа з однаковим hashCode знаходяться поруч). Тоді можна дуже швидко і легко відкинути непотрібні групи, достатньо один раз для кожної групи перевірити, чи її hashCode збігається з hashCode заданого об'єкта.
Уяви, що ти студент, і шукаєш свого друга, якого знаєш в обличчя і про якого відомо, що він живе у 17-му гуртожитку. Тоді ти просто проходишся всім гуртожиткам універу і в кожному гуртожитку запитуєш «це 17-й гуртожиток?». Якщо ні, то ти відкидаєш усіх із цього гуртожитку і переходиш до наступного. Якщо так, то починаєш ходити по всіх кімнатах і шукати друга.
У даному прикладі номер гуртожитку – 17 – це і є hashCode.
Розробник, який реалізує функцію hashCode, має знати такі речі:
А) у двох різних об'єктів може бути однаковий hashCode (різні люди можуть жити у одному гуртожитку)
Б) у однакових об'єктів (з точки зору equals) має бути однаковий hashCode.
В) хеш-коди повинні бути обрані таким чином, щоб не було великої кількості різних об'єктів з однаковими hashCode. Це зведе всі їхні переваги нанівець (Ти прийшов у 17-й гуртожиток а там живе половина універа).
І тепер найважливіше. Якщо ти перевизначаєш метод equals, обов'язково потрібно перевизначити метод hashCode(), з урахуванням трьох правил, що описані вище.
Справа в тому, що колекції в Java перед тим як порівняти об'єкти за допомогою equals завжди шукають/порівнюють їх за допомогою методу hashCode(). І якщо у однакових об'єктів будуть різні hashCode, то об'єкти будуть вважатися різними – до порівняння за допомогою equals просто не дійде.
У нашому прикладі з Дробом, якби ми взяли hashCode рівний чисельнику, дроби 2/3 і 4/6 мали б різні hashCode. Дроби – однакові, equals каже, що вони однакові, але hashCode каже, що вони різні. І якщо перед порівнянням за допомогою equals порівнювати за hashCode, отримаємо, що об'єкти різні і до equals просто не дійдемо.
Приклад:
HashSet<Дріб>set = new HashSet<Дріб>(); set.add(new Дріб(2,3)); System.out.println( set.contains(new Дріб(4,6)) ); |
Якщо метод hashCode() буде повертати чисельник дробу, то результат буде false. Об'єкт new Дріб(4,6) не буде знайдений у колекції. |
— А як правильно реалізувати hashCode для дробу?
— Тут треба пам'ятати, що однаковим дробам обов'язково має відповідати однаковий hashCode.
Варіант 1: hashCode дорівнює цілій частині від поділу.
Для дробу 7/5 і 6/5 це буде 1.
Для дробу 4/5 і 3/5 це буде 0.
Але цей варіант погано підходить для порівняння дробів, які явно менше 1. Ціла частина (hashCode) завжди буде 0.
Варіант 2: hashCode дорівнює цілій частині від поділу знаменника на чисельник.
Цей варіант підійде для випадку, коли значення дробу менше 1. Якщо дріб менше 1, значить перевернутий дріб більше 1. А якщо ми перевертаємо всі дроби – це ніяк не позначиться на їх порівнянні.
Підсумковий варіант поєднуватиме в собі обидва рішення
public int hashCode()
{
return чисельник/знаменник + знаменник/чисельник;
}
Перевіряємо для дробів 2/3 і 4/6. У них має бути однаковий hashCode:
Дріб 2/3 | Дріб 4/6 | |
---|---|---|
чисельник / знаменник | 2 / 3 == 0 | 4 / 6 == 0 |
знаменник / чисельник | 3 / 2 == 1 | 6 / 4 == 1 |
чисельник / знаменник + знаменник / чисельник |
0 + 1 == 1 | 0 + 1 == 1 |
На цьому – все.
— Дякую, Еллі, було справді цікаво.