1. Порівняння об'єктів у Java
Об'єкти у Java можна порівнювати як за посиланням, так і за значенням.
Порівняння посилань
Якщо дві змінні вказують на один і той же об'єкт у пам'яті, то посилання, які зберігаються у цих змінних, рівні. Якщо порівняти такі змінні за допомогою оператора рівності ==, ви отримаєте true, що логічно. Тут все просто.
| Код | Вивід на екран |
|---|---|
|
|
Порівняння за значенням
Однак часто можна зустріти ситуацію, коли дві змінні посилаються на два різних, але ідентичних об'єкти. Наприклад, два рядки, які містять однаковий текст, але знаходяться у різних об'єктах.
Для визначення ідентичності різних об'єктів потрібно використовувати метод equals() Приклад:
| Код | Вивід на екран |
|---|---|
|
|
Метод equals є не тільки у класу String — він є у взагалі всіх класів.
Навіть у тих, які ви тільки будете писати, і ось чому.
2. Клас Object
Усі класи в Java вважаються успадкованими від класу Object. Це розробники Java так придумали.
А якщо якийсь клас успадкований від класу Object, у цьому класі-нащадку з'являються всі методи класу Object. Це і є головний ефект успадкування.
Іншими словами, у кожного класу, навіть якщо це не написано в його коді, є всі методи, які є у класу Object.
А серед таких методів є методи, які мають відношення до порівняння об'єктів. Це метод equals() і метод hashCode().
| Код | Як буде насправді: |
|---|---|
|
|
У прикладі вище ми створили простий клас Person із параметрами name і age, без жодного методу. Однак, оскільки всі класи вважаються успадкованими від класу Object, у класу Person приховано з'явилися два методи:
| Метод | Опис |
|---|---|
|
Порівнює поточний об'єкт і переданий об'єкт |
|
Повертає hash-code поточного об'єкта |
Виходить, що методи equals є у абсолютно всіх об'єктів і можна порівнювати між собою об'єкти різних типів, і це все буде чудово компілюватися та працювати.
| Код | Вивід на екран |
|---|---|
|
|
|
|
3. Метод equals()
Успадкований від класу Object метод equals() містить найпростіший алгоритм порівняння поточного і переданого об'єктів — він просто порівнює їхні посилання.
Той самий ефект ви отримаєте, якщо просто порівняєте змінні класу Person замість виклику методу equals(). Приклад:
| Код | Вивід на екран |
|---|---|
|
|
Метод equals просто порівнює у себе всередині посилання a і b.
Однак у класу String порівняння працює інакше. Чому?
Тому що розробники класу String написали власну реалізацію методу equals().
Реалізація методу equals()
Давайте і ми напишемо свою реалізацію методу equals в класі Person. Розберемо 4 основних випадки.
equals, він завжди приймає параметр типу
Object
Сценарій 1: у метод equals передали той самий об'єкт, у якого викликали метод equals. Якщо посилання у поточного і переданого об'єктів рівні, потрібно повернути true. Об'єкт збігається сам із собою.
У коді це буде виглядати так:
| Код | Опис |
|---|---|
|
Порівнюємо посилання |
Сценарій 2: у метод equals передали посилання null — порівнювати нема з чим. Об'єкт, у якого викликали метод equals, точно не null, отже, у цьому випадку потрібно повернути false.
У коді це буде виглядати так:
| Код | Опис |
|---|---|
|
Порівнюємо посилання Переданий об'єкт — null? |
Сценарій 3: у метод equals передали посилання на об'єкт взагалі не класу Person. Чи рівний об'єкт класу Person об'єкту класу не-Person? Тут вже вирішує сам розробник класу Person — як хоче, так і зробить.
Але зазвичай все ж таки об'єкти вважаються рівними, якщо це об'єкти одного класу. Тому якщо в наш метод equals передали об'єкт не класу Person, ми будемо завжди повертати false. А як перевірити, якого типу об'єкт? Правильно: за допомогою оператора instanceof.
Ось як буде виглядати наш новий код:
| Код | Опис |
|---|---|
|
Порівнюємо посилання Переданий об'єкт — null? Якщо переданий об'єкт не типу Person |
4. Порівняння двох об'єктів Person
Що ми отримали в результаті? Якщо ми дійшли до кінця методу, значить, у нас об'єкт типу Person і посилання не null. Тоді перетворюємо його до типу Person і будемо порівнювати внутрішні частини обох об'єктів. Це і є наш сценарій номер 4.
| Код | Опис |
|---|---|
|
Порівнюємо посилання Переданий об'єкт — null? Якщо переданий об'єкт не типу Person Операція приведення типу |
А як порівнювати два об'єкти Person? Вони рівні, якщо у них рівні імена (name) і вік (age). Остаточний код буде виглядати так:
| Код | Опис |
|---|---|
|
Порівнюємо посилання Переданий об'єкт — null? Якщо переданий об'єкт не типу Person Операція приведення типу |
Але і це ще не все.
По-перше, поле name має тип String, а значить, поля name потрібно порівнювати за допомогою виклику методу equals.
this.name.equals(person.name)
По-друге, поле name цілком може бути рівним null: тоді викликати метод equals у нього не можна. Потрібна додаткова перевірка на null:
this.name != null && this.name.equals(person.name)
Однак якщо name дорівнює null в обох об'єктах Person, імена все-таки рівні.
Код четвертого сценарію може виглядати, наприклад, так:
|
Якщо вік не рівний, одразу return false Якщо this.name дорівнює null, немає сенсу порівнювати через equals. Тут або друге поле name дорівнює null, або ні. Порівнюємо два поля name через equals. |
5. Метод hashCode()
Окрім методу equals, який виконує детальне порівняння всіх полів обох об'єктів, є ще один метод, який може використовуватися для неточного, але дуже швидкого порівняння — hashCode().
Уявіть, що ви сортуєте в алфавітному порядку список із тисяч слів, і вам потрібно постійно попарно порівнювати слова. А слова довгі, і букв у них багато. У загальному таке порівняння буде йти дуже довго.
Однак його можна пришвидшити. Припустимо, слова починаються на різні букви: одразу ж зрозуміло, що вони різні. От якщо вони починаються на однакові букви, тоді гарантій немає: у подальшому слова можуть виявитися або рівними, або різними.
Метод hashCode() працює за схожим принципом. Якщо його викликати у об'єкта, то він поверне яке-небудь число — аналог першої букви в слові. Це число має такі властивості:
- У однакових об'єктів завжди однакові hash-code
- У різних об'єктів можуть бути однакові hash-code, а можуть бути різні
- Якщо у об'єктів різні hash-code, об'єкти точно різні
Для кращого розуміння перепишемо ці властивості стосовно слів:
- У однакових слів завжди однакові перші букви
- У різних слів можуть бути однакові перші букви, а можуть бути і різні
- Якщо у слів різні перші букви, слова точно різні
Остання властивість і використовується для пришвидшеного порівняння об'єктів:
Спочатку у двох об'єктів обчислюються hash-code. Якщо ці hash-code різні, то об'єкти точно різні, і порівнювати їх далі не потрібно.
А от якщо hash-code однакові, доведеться все ж порівнювати об'єкти за допомогою equals.
6. Контракти в коді
Описана вище поведінка має бути реалізована у всіх класах у Java. Перевірити правильність порівняння об'єктів на рівні компіляції ніяк не можна.
Усі Java-програмісти домовилися, що якщо вони пишуть свою реалізацію методу equals() замість стандартної (з класу Object), вони також повинні написати свою реалізацію методу hashCode(), щоб озвучені вище правила зберігалися.
Така домовленість називається контрактом.
Якщо ви додаєте у свій клас лише реалізацію одного методу equals() або лише hashCode(), ви грубо порушуєте контракт (порушуєте домовленість). Так робити не можна.
Якщо інші програмісти будуть використовувати ваш код, він може працювати неправильно. Більше того, ви теж будете використовувати код, який працює на основі вищеозвучених контрактів.
Усі колекції в Java при пошуку елемента всередині колекції спочатку порівнюють hash-code об'єктів, а тільки потім викликають для порівняння метод equals.
Тому якщо ви напишете свій клас, а в ньому нову функцію equals, але не напишете метод hashCode() або реалізуєте його з помилками, колекції можуть неправильно працювати з вашими об'єктами.
Наприклад, ви додали об'єкт у список, потім шукаєте його за допомогою методу contains(), а колекція ваш об'єкт не знаходить.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ