— Тепер я розповім про не менш корисні методи equals(Object o) & hashCode().

Як ти вже, напевно, встиг запам'ятати, у Java при порівнянні посилальних змінних порівнюються не самі об'єкти, а посилання на об'єкти.

Код Пояснення
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i==j);
i не дорівнює j
Змінні вказують на різні об'єкти.
Хоча об'єкти містять однакові дані;
Integer i = new Integer(1);
Integer j = i;
System.out.println(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

На цьому – все.

— Дякую, Еллі, було справді цікаво.