Назад Зміст Вперед

7.5. Перевизначення методів toString(), equals() класу Object


Перевизначення toString() та equals()

Все в Java є об’єктом. Не просто об’єктом, а Об’єктом (Object) з великої букви. Кожен виняток, кожна подія, кожен масив поширює java.lang.Object.

Методи класу Object



Метод
Опис
boolean equals
          (Object obj)
Визначає, чи 2 об’єкти є еквівалентними
void finalize()
Викликається збирачем сміття у випадку відсутності посилань на об’єкт
int hashCode()
Повертає int значення хеш-коду об’єкта, після чого об’єкт може бути використаний в класах Collection - Hashtable, HashMap,  HashSet
final void notify()
Виводить зі стану очікування потік, що чекає на замок цього об’єкта.
final void notifyAll()
Виводить зі стану очікування всі потоки, які чекають на замок цього об’єкта.
final void wait()
Поточний потік чекає виклику іншим потоком методів notify() або notifyAll() на цьому об’єкті
String toString()
Повертає «текстове представлення» об’єкту

Метод toString()

Перевизначення toString() дозволяє прочитати щось змістовне про об’єкт створеного класу. Код може викликати toString() для об’єкту при необхідності прочитати корисні деталі про об’єкт. Наприклад, при передаванні посилання на об’єкт методу System.out.println(), викликається метод toString() класу Оbject, а повернення toString() показано в наступному прикладі:
public class HardToRead {
public static void main (String [] args) {
HardToRead h = new HardToRead();
System.out.println(h);
}
}

Виконання класу HardToRead виводить:
% java HardToRead
HardToRead@a47e0

Попередній вивід буде отримано, якщо метод toString() класу Object не є перевизначеним. В такому випадку після символу @ отримується ім’я класу, а далі - беззнакове шістнадцяткове представлення хешкоду об’єкту.
Намагання прочитати цей вивід мотивує перевизначити метод toString() в класі, наприклад, наступним чином:

public class BobTest {
public static void main (String[] args) {
Bob f = new Bob("GoBobGo", 19);
System.out.println(f);
}
}
class Bob {
int shoeSize;
String nickName;
Bob(String nickName, int shoeSize) {
this.shoeSize = shoeSize;
this.nickName = nickName;
}
public String toString() {
return ("I am a Bob, but you can call me " +

nickName + ". My shoe size is " + shoeSize);
}
}

Вивід читається вже краще:
% java BobTest
I am a Bob, but you can call me GoBobGo. My shoe size is 19

Загальне виконання toString() просто виводить поточні значення важливих змінних об’єкту.

Перевизначення equals()

Порівняння посилань на два об’єкти оператором = = формує true, коли два посилання відносяться до одного об’єкта (тому що оператор = = просто аналізує ідентичність бітів). В класах String і Wrapper метод equals(), унаслідуваний від класу Object, є перевизначеним, отже можна порівнювати два об’єкти одного типу для аналізу еквівалентності їх змісту. Якщо два екземпляри Integer є еквівалентними, якщо обидва мають int значення 5. Факт наявності значень 5 в двох різних об’єктах не є важливим.

Коли дійсно потрібно знати, чи два посилання є ідентичними, необхідно використовувати = =. Але коли потрібно визначити рівність об’єктів, а не посилань на них, застосовують метод equals(). Для кожного класу необхідно визначити сенс у розгляді еквівалентності двох різних екземплярів. Об’єкти деяких класів можуть ніколи не бути рівними. Наприклад, уявимо клас Car із такими змінними екземпляру, як виробник, модель, рік, конфігурація — звичайно, жодна машина є унікальною. Отже, дві машини ніколи не мають сприйматися абсолютно однаковими. Якщо два посилання відносяться до однієї машини, це означає, що вони обидва говорять про одну машину, а не про дві з однаковими атрибутами. Таким чином, для Car, можливо, не потрібно перевизначати метод equals().

Що буде при неперевизначенні equals()

Потенційне обмеження для неперевизначеного методу equals() класу: стає неможливим використовувати об’єкти цього класу в якості ключових в хеш-таблиці, а також, напевно, не вдасться отримати точних Sets, що концептуально не мають дублікатів.

Метод equals() в класі Object використовує для порівняння тільки оператор = =, отже поки метод equals()не є перевизначеним, два об’єкта будуть вважатися рівними тільки у випадку, якщо два посилання відносяться до одного об’єкта.
Розглянемо, що означає неможливість використати об’єкт в якості ключа хеш-таблиці. Уявимо, що HashMap містить машини, специфічні екземпляри яких є ключами для пошуку власника - об’єкту Person (наприклад, червона Subaru Outback назве Джона, а фіолетова Mini виведе на Мері). Отже, додавання екземпляру машини є в HashMap ключем до відповідного об’єкту Person як значення. При виконанні пошуку HashMap отримує команду: "Ось машина, знайди відповідний об’єкт Person". Але проблема в тому, що є посилання тільки на той об’єкт, який використано в якості ключа при додаванні до HashMap. Іншими словами, неможливо створити ідентичний об’єкт Car і використовувати його для пошуку.
На практиці, якщо необхідне використання об’єктів класу в якості ключів хеш-таблиці (або в будь-якій іншій структурі даних, що використовує еквівалентність для пошуку і/або повернення об’єкта), потрібно перевизначити equals(), щоб два різні екземпляри вважались одним.

Як зафіксувати машину? Можна перевизначити метод equals() таким чином, що він порівнюватиме унікальні транспортні ідентифікаційні номера. В такому випадку можна використати один екземпляр для додавання його до HashMap, і створити ще один екземпляр для пошуку із використанням його в якості ключа. Звичайно, перевизначення методу equals() для Car потенційно дозволить більш, ніж одному об’єкту представляти унікальну машину, що зменшує безпеку проекту. На щастя, класи String і Wrapper добре працюють як ключі в хеш-таблицях—вони перевизначають метод equals(). Тому замість використання дійсного екземпляру Car в якості ключа в парі машина/власник, можна просто використати String, що представляє унікальний ідентифікатор машини. В цьому випадку є тільки один екземпляр, що представляє унікальну машину, але досі можна використовувати один з атрибутів машини в якості ключа для пошуку.

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

Перевизначення equals() в класі може виглядати наступним чином:
public class EqualsTest {
public static void main (String [] args) {
Moof one = new Moof(8);
Moof two = new Moof(8);
if (one.equals(two)) {
System.out.println("one and two are equal");
}
}
}
class Moof {
private int moofValue;
Moof(int val) {
moofValue = val;
}
public int getMoofValue() {
return moofValue;
}
public boolean equals(Object o) {
if((o instanceof Moof) && (((Moof)o).getMoofValue()

== this.moofValue)) {
return true;
} else {
return false;
}
}
}

Детально розглянемо цей код. В методі main() класу EqualsTest створено два екземпляра Moof, в конструктори яких передано однакове значення 8. В класі Moof аргумент конструктора задає значення об’єктній змінній moofValue. Уявимо, що два об’єкта Moof є однакові, якщо їхні змінні moofValue є ідентичними. Тоді перевизначений метод equals() повинен порівнювати змінні moofValues. Це так просто. Але давайте зупинимось і подивимось що трапилось в методі equals():

1. public boolean equals(Object o) {
2. if((o instanceof Moof)&&(((Moof)o).getMoofValue()== this.moofValue)) {
3. return true;
4. } else {
5. return false;
6. }
7. }

Перш за все, необхідно дотримуватись всіх правил перевизначення, тому в лінійці 1 задеклароване перевизначення методу equals(), унаслідуваного від Object.
В лінійці 2 відбувається головне дійство. Логічною є необхідність виконання двох пунктів у створенні коректного порівняння для визначення еквівалентності.
Спочатку необхідно переконатись у коректному типі об’єкту, що тестується! Він надходить як поліморфічний тип Object, тому потрібно його протестувати із використанням instanceof. Впевненість у еквівалентності двох об’єктів різних класів є необгрунтованою, проте ці проблеми проектів тут не розглядаються. Між іншим, тест instanceof тест виконується тільки для обґрунтування можливості перетворити аргумент об’єкту в коректний тип і отримання доступу до його методів або змінних з метою дійсно зробити порівняння. Пам’ятайте, якщо об’єкт не витримує тест instanceof, викликається виняток ClassCastException. Наприклад:
public boolean equals(Object o) {
if (((Moof)o).getMoofValue() == this.moofValue){

//попередня лінійка компілюється,проте є поганою!
return true;
} else {
return false;
}
}

Перетворення (Moof)o не виконається, якщо o посилається на щось, що не є IS-A Moof.

По-друге, необхідно порівняти атрибути, про які йде мова (в цьому випадку, moofValue). Тільки програміст може вирішати, що саме робить два екземпляри рівними. (Для підвищення швидкодії краще перевіряти меншу кількість атрибутів.)
Щодо трохи дивного синтаксису ((Moof)o).getMoofValue(), то тут просто перетворене посилання на об’єкт o для виклику методу, що є в класі Moof, але відсутній у Object. Пам’ятайте, що без перетворення компіляція є неможливою, тому що компілятор бачить об’єкт Object, в якому немає методу moofValue(). Проте під час виконання, навіть із перетворенням, код зупиниться, якщо об’єкт о буде посилатись на щось, що не може перетворюватись до Moof. Тож ніколи не забувайте користуватися тестом instanceof спочатку. Ще один варіант використання оператора && — якщо тест instanceof не дотримано, код із перетворенням не виконується, тобто, завжди безпечним є наступне:

if ((o instanceof Moof) && (((Moof)o).getMoofValue()
== this.moofValue)) {
return true;
} else {
return false;
}

Отже, необхідно вважати на метод equals()…
В специфікації Java API класу Object є те, що називається контрактом методу equals(). Java контракт є набором правил, яких потрібно дотримуватись для коректного виконання коду разом із іншими елементами. Або, з іншого боку, якщо не дотримуватись контракту, код все одно буде компілюватись та виконуватись, але під час виконання може неочікувано зупинитись.

Пам’ятайте що методи equals(), hashCode(), toString() є public. Наступне не буде коректним перевизначенням методу equals(), хоча може бути використаним:

class Foo { boolean equals(Object o) { } }

Також слідкуйте за типами аргументу. Наступний метод є перевантаженням, але не перевизначенням методу equals():

class Boo { public boolean equals(Boo b) { } }

Метод equals() в класі Boo змінює аргумент з Object на Boo, і стає перевантаженим методом, тобто, не може бути викликаним ані звідки, окрім як із власного коду, де відомо про цей новий інший метод, назва якого співпала із equals().

Контракт equals()
Документація Java для контракту equals() встановлює наступне:
  • Метод є рефлексивним. Для будь-якого значення посилання x x.equals(x) повинен повертати true.
  • Метод є симетричним. Для будь-яких значень посилань x, y виклик x.equals(y) поверне true, якщо і тільки якщо y.equals(x) поверне true.
  • Метод є транзитивним. Для будь-яких значень посилань x, y, z, якщо x.equals(y) повертає true і y.equals(z) повертає true, тоді і x.equals(z) поверне true.
  • Метод є послідовним. Для будь-яких значень посилань x,y, якщо багаторазовий виклик x.equals(y) послідовно повертає true або послідовно повертає false, у випадку зміни об’єкту для порівняння еквівалентності не задіяна відповідна інформація.§ Для будь-якого значення посилання x, що не дорівнює null, x.equals(null) поверне false.
Проте, цього ще недостатньо. Ще не розглянуто метод hashCode(), який пов'язаний із equals() контрактом, де визначено наступне: якщо два об’єкти є визнані рівними методом equals(), то вони повинні мати ідентичні значення хеш-коду. Для дійсної безпеки при перевизначенні equals() необхідно перевизначати hashCode() також.



.