1. Порівняння об'єктів у Java

Об'єкти в Java можна порівнювати як за посиланням, так і за значенням.

Порівняння посилань

Якщо дві змінні вказують на один і той самий об'єкт у пам'яті, то посилання, що зберігаються в цих змінних, — однакові (рівні). Якщо порівняти такі змінні за допомогою оператора порівняння ==, отримаємо true, і це логічно. Тут усе просто.

Код Виведення на екран
Integer a = 5;
Integer b = a;
System.out.println(a == b);


true

Порівняння за значенням

Однак часто трапляються ситуації, коли дві змінні посилаються на два різні, але ідентичні за змістом об'єкти. Приміром, на два рядки, що містять однаковий текст, але знаходяться в різних об'єктах.

Для визначення ідентичності різних об'єктів потрібно використовувати метод equals(). Приклад:

Код Виведення на екран
String a = new String("Привіт");
String b = new String("Привіт");
System.out.println(a == b);
System.out.println(a.equals(b));


false
true

Метод equals є не лише у класу String — він є взагалі у всіх класів.

Навіть у тих, які ви ще тільки будете писати. І от чому.


8
Задача
Модуль 1. Java Syntax,  16 рівень3 лекція
Недоступна
Створюємо свій список
ArrayList — це список, який зберігає динамічно розширюваний масив елементів. Зараз ми будемо реалізовувати свою версію списку, в якому зберігаються рядки. Клас CustomStringArrayList має три поля: String[] elements — це масив поточних елементів (у схожому масиві зберігає значення ArrayList). Початко

2. Клас Object

Усі класи в Java вважаються успадкованими від класу Object. Це автори мови Java так придумали.

А якщо якийсь клас успадковано від класу Object, у цьому класі-спадкоємцеві з'являються всі методи класу Object. Це і є головним ефектом успадкування.

Інакше кажучи, кожен клас, навіть якщо це не написано в його коді, має всі методи, які має клас Object.

А серед цих методів є такі, що стосуються порівняння об'єктів. Це метод equals() і метод hashCode().

Код Як буде насправді:
class Person
{
   String name;
   int age;
}
class Person extends Object
{
   String name;
   int age;

   public boolean equals(Object obj)
   {
      return this == obj;
   }

   public int hashCode()
   {
      return адреса_об'єкта_в_пам'яті;         //це реалізація за замовчуванням, але можлива й інша.
   }
}

У прикладі вище ми створили простий клас Person з параметрами name і age без жодного методу. Проте, оскільки всі класи вважаються успадкованими від класу Object, у класу Person приховано з'явилися два методи:

Метод Опис
boolean equals(Object obj)
Порівнює поточний об'єкт і переданий об'єкт
int hashCode()
Повертає hash-code поточного об'єкта

Отже, методи equals є абсолютно в усіх об'єктів і можна порівнювати між собою об'єкти різних типів, і це все чудово компілюватиметься й працюватиме.

Код Виведення на екран
Integer a = 5;
String s = "Привіт";
System.out.println(a.equals(s));
System.out.println(s.equals(a));


false
false
Object a = new Integer(5);
Object b = new Integer(5);
System.out.println(a.equals(b)) ;


true

3. Метод equals()

Успадкований від класу Object метод equals() містить найпростіший алгоритм порівняння поточного й переданого об'єктів — він просто порівнює їхні посилання.

Той самий ефект ви отримаєте, якщо просто порівняєте змінні класу Person замість виклику методу equals(). Приклад:

Код Виведення на екран
Person a = new Person();
a.name = "Ганнуся";

Person b = new Person();
b.name = "Ганнуся";

System.out.println(a == b);
System.out.println(a.equals(b));






false
false

Метод equals просто порівнює всередині себе посилання a і b.

Однак у класі String порівняння працює інакше. Чому?

Тому що розробники класу String написали власну реалізацію методу equals().

Реалізація методу equals()

Спробуймо написати свою реалізацію методу equals у класі Person. Розгляньмо 4 основні випадки.

Важливо!
Незалежно від того, для якого класу перевизначається метод equals, він завжди отримує параметр типу Object

Сценарій 1: у метод equals передано той самий об'єкт, для якого було викликано метод equals. Якщо посилання поточного й переданого об'єктів рівні, слід повернути true. Об'єкт збігається сам із собою.

Відповідний код матиме такий вигляд:

Код Опис
public boolean equals(Object obj)
{
   if (this == obj)
    return true;

   решта коду методу equals
}


Порівнюємо посилання

Сценарій 2: у метод equals передано посилання null — порівнювати немає з чим. Об'єкт, для якого викликано метод equals, — точно не null, значить, у цьому разі слід повернути false.

Відповідний код матиме такий вигляд:

Код Опис
public boolean equals(Object obj)
{
   if (this == obj)
      return true;

   if (obj == null)
      return false;

   решта коду методу equals
}


Порівнюємо посилання


Переданий об'єкт — null?

Сценарій 3: у метод equals передано посилання на об'єкт, який взагалі не належить до класу Person. Чи дорівнює об'єкт класу Person об'єкту класу не-Person? Це вже вирішує сам розробник класу Person — як схоче, так і зробить.

Але зазвичай об'єкти вважаються рівними тоді, коли вони належать до одного класу. Отож якщо в наш метод equals передано об'єкт не класу Person, ми завжди повертатимемо false. А як перевірити тип об'єкта? Правильно: за допомогою оператора instanceof.

Отакий вигляд матиме наш новий код:

Код Опис
public boolean equals(Object obj)
{
   if (this == obj)
      return true;

   if (obj == null)
      return false;

   if ( !(obj instanceof Person) )
      return false;

   решта коду методу equals
}


Порівнюємо посилання


Переданий об'єкт — null?


Якщо тип переданого об'єкта не Person

4. Порівняння двох об'єктів Person

Що ми отримали в результаті? Якщо ми дійшли до кінця методу, значить, маємо об'єкт типу Person і посилання не null. Тоді перетворюємо наш об'єкт на тип Person і будемо порівнювати вміст обох об'єктів. Це і є наш сценарій номер 4.

Код Опис
public boolean equals(Object obj)
{
   if (this == obj)
      return true;

   if (obj == null)
      return false;

   if ( !(obj instanceof Person) )
      return false;

   Person person = (Person) obj;

   решта коду методу equals
}


Порівнюємо посилання


Переданий об'єкт — null?


Якщо тип переданого об'єкта не Person


Операція перетворення типу

А як порівнювати два об'єкти типу Person? Вони рівні, якщо в них однакові імена (name) і вік (age). Остаточний вигляд коду буде таким:

Код Опис
public boolean equals(Object obj)
{
   if (this == obj)
      return true;

   if (obj == null)
      return false;

   if ( !(obj instanceof Person) )
      return false;

   Person person = (Person) obj;

   return this.name == person.name && this.age == person.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, то імена все-таки однакові.

Код четвертого сценарію може мати, приміром, такий вигляд:

Person person = (Person) obj;

if (this.age != person.age)
   return false;

if (this.name == null)
   return person.name == null;

return this.name.equals(person.name);


Якщо вік неоднаковий,
одразу 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(), а колекція ваш об'єкт не знаходить.