equals()
и hashCode()
. Това не е първият път, когато ги срещаме: курсът CodeGym започва с кратък урок за equals()
— прочетете го, ако сте го забравor or не сте го виждали преди... 
==
оператора, защото ==
сравнява препратки. Ето нашия пример с автомобor от скорошен урок:
public class Car {
String model;
int maxSpeed;
public static void main(String[] args) {
Car car1 = new Car();
car1.model = "Ferrari";
car1.maxSpeed = 300;
Car car2 = new Car();
car2.model = "Ferrari";
car2.maxSpeed = 300;
System.out.println(car1 == car2);
}
}
Конзолен изход:
false
Изглежда, че сме създали два идентични Car
обекта: стойностите на съответните полета на двата автомобилни обекта са еднакви, но резултатът от сравнението все още е неверен. Вече знаем причината: референциите car1
и car2
сочат към различни addressи на паметта, така че не са равни. Но все пак искаме да сравним двата обекта, а не две препратки. Най-доброто решение за сравняване на обекти е equals()
методът.
метод equals().
Може би си спомняте, че ние не създаваме този метод от нулата, по-скоро го заместваме: методътequals()
е дефиниран в Object
класа. Въпреки това, в обичайната си форма, той е от малка полза:
public boolean equals(Object obj) {
return (this == obj);
}
Ето How equals()
методът е дефиниран в Object
класа. Това е още веднъж сравнение на препратките. Защо са го направor така? Е, How създателите на езика знаят кои обекти във вашата програма се считат за равни и кои не? :) Това е основната точка на equals()
метода — създателят на клас е този, който определя кои характеристики да се използват при проверка на equalsството на обектите от класа. След това замествате equals()
метода във вашия клас. Ако не разбирате съвсем meaningто на „определя кои характеристики“, нека разгледаме един пример. Ето един прост клас, представляващ мъж: Man
.
public class Man {
private String noseSize;
private String eyesColor;
private String haircut;
private boolean scars;
private int dnaCode;
public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
this.noseSize = noseSize;
this.eyesColor = eyesColor;
this.haircut = haircut;
this.scars = scars;
this.dnaCode = dnaCode;
}
// Getters, setters, etc.
}
Да предположим, че пишем програма, която трябва да определи дали двама души са еднояйчни близнаци or просто си прorчат. Имаме пет характеристики: размер на носа, цвят на очите, прическа, наличие на белези и резултати от ДНК тестове (за простота представяме това като цяло число). Коя от тези характеристики смятате, че ще позволи на нашата програма да идентифицира еднояйчни близнаци? 
equals()
метод? Трябва да го отменим вMan
клас, като се вземат предвид изискванията на нашата програма. Методът трябва да сравнява int dnaCode
полето на двата обекта. Ако са равни, значи обектите са равни.
@Override
public boolean equals(Object o) {
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Наистина ли е толкова просто? Не точно. Пропуснахме нещо. За нашите обекти ние идентифицирахме само едно поле, което е от meaning за установяване на equalsство на обекти: dnaCode
. Сега си представете, че имаме не 1, а 50 релевантни полета. И ако всички 50 полета на два обекта са равни, тогава обектите са равни. Възможен е и такъв сценарий. Основният проблем е, че установяването на equalsство чрез сравняване на 50 полета е времеемък и ресурсоемък процес. Сега си представете, че в допълнение към нашия Man
клас имаме Woman
клас с точно същите полета, които съществуват в Man
. Ако друг програмист използва нашите класове, той or тя лесно може да напише code като този:
public static void main(String[] args) {
Man man = new Man(........); // A bunch of parameters in the constructor
Woman woman = new Woman(.........); // The same bunch of parameters.
System.out.println(man.equals(woman));
}
В този случай проверката на стойностите на полетата е безсмислена: лесно можем да видим, че имаме обекти от два различни класа, така че няма начин те да бъдат равни! Това означава, че трябва да добавим проверка към equals()
метода, сравнявайки класовете на сравняваните обекти. Добре че се сетихме за това!
@Override
public boolean equals(Object o) {
if (getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Но може би сме забравor нещо друго? Хм... Като минимум трябва да проверим дали не сравняваме обект със самия него! Ако препратки A и B сочат към един и същ address на паметта, тогава те са един и същ обект и не е нужно да губим време и да сравняваме 50 полета.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Също така не боли да добавите проверка за null
: нито един обект не може да бъде equals на null
. Така че, ако параметърът на метода е нула, тогава няма смисъл от допълнителни проверки. Имайки предвид всичко това, нашият equals()
метод за Man
класа изглежда така:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Извършваме всички първоначални проверки, споменати по-горе. В края на деня, ако:
- ние сравняваме два обекта от един и същи клас
- и сравняваните обекти не са един и същ обект
- и преминалият обект не е
null
dnaCode
полетата на двата обекта. Когато отменяте equals()
метода, не забравяйте да спазвате следните изисквания:
-
Рефлексивност.
Когато
equals()
методът се използва за сравняване на обект със себе си, той трябва да върне true.
Вече сме изпълнor това изискване. Нашият метод включва:if (this == o) return true;
-
Симетрия.
Ако
a.equals(b) == true
, тогаваb.equals(a)
трябва да се върнеtrue
.
Нашият метод отговаря и на това изискване. -
Преходност.
Ако два обекта са равни на няHowъв трети обект, тогава те трябва да са равни един на друг.
Акоa.equals(b) == true
иa.equals(c) == true
, тогаваb.equals(c)
също трябва да върне true. -
Упоритост.
Резултатът от
equals()
трябва да се променя само когато участващите полета се променят. Ако данните на двата обекта не се променят, тогава резултатътequals()
трябва винаги да е един и същ. -
Неequalsство с
null
.За всеки обект
a.equals(null)
трябва да върне false
Това не е просто набор от някои "полезни препоръки", а по-скоро строг договор , изложен в documentацията на Oracle
метод hashCode().
Сега нека поговорим заhashCode()
метода. Защо е необходимо? Точно за същата цел - да сравнявате обекти. Но ние вече имаме equals()
! Защо друг метод? Отговорът е прост: за подобряване на производителността. Хеш функция, представена в Java с помощта на hashCode()
метода, връща числова стойност с фиксирана дължина за всеки обект. В Java hashCode()
методът връща 32-битово число ( int
) за всеки обект. Сравняването на две числа е много по-бързо от сравняването на два обекта с помощта на equals()
метода, особено ако този метод взема предвид много полета. Ако нашата програма сравнява обекти, това е много по-лесно да се направи с помощта на хеш code. Само ако обектите са еднакви въз основа на hashCode()
метода, сравнението продължава къмequals()
метод. Между другото, така работят структурите от данни, базирани на хеш, например познатите HashMap
! Методът hashCode()
, подобно на equals()
метода, се отменя от разработчика. И точно като equals()
, hashCode()
методът има официални изисквания, посочени в documentацията на Oracle:
-
Ако два обекта са еднакви (т.е.
equals()
методът връща true), тогава те трябва да имат еднакъв хеш code.В противен случай нашите методи биха бor безсмислени. Както споменахме по-горе,
hashCode()
първо трябва да се извърши проверка, за да се подобри производителността. Ако хеш codeовете бяха различни, тогава проверката щеше да върне false, въпреки че обектите всъщност са равни според начина, по който сме дефинирали методаequals()
. -
Ако
hashCode()
методът се извика няколко пъти на един и същи обект, той трябва да връща едно и също число всеки път. -
Правило 1 не работи в обратната посока. Два различни обекта могат да имат един и същ хеш code.
hashCode()
връща int
. An int
е 32-битово число. Има ограничен диапазон от стойности: от -2,147,483,648 до +2,147,483,647. С други думи, има малко над 4 мorарда възможни стойности за int
. Сега си представете, че създавате програма за съхраняване на данни за всички хора, живеещи на Земята. Всеки човек ще отговаря на свой собствен Person
обект (подобно на Man
класа). На планетата живеят ~7,5 мorарда души. С други думи, без meaning колко умен е алгоритъмът, който пишем за конвертиранеPerson
обекти към int, просто нямаме достатъчно възможни числа. Имаме само 4,5 мorарда възможни int стойности, но има много повече хора от това. Това означава, че колкото и да се опитваме, някои различни хора ще имат еднакви хеш codeове. Когато това се случи (хеш codeовете съвпадат за два различни обекта) го наричаме сблъсък. Когато заменя hashCode()
метода, една от целите на програмиста е да минимизира потенциалния брой сблъсъци. Отчитайки всички тези правила, How ще hashCode()
изглежда методът в Person
класа? Като този:
@Override
public int hashCode() {
return dnaCode;
}
изненадан? :) Ако погледнете изискванията ще видите, че спазваме всички. Обектите, за които нашият equals()
метод връща true, също ще бъдат равни според hashCode()
. Ако нашите два Person
обекта са равни по equals
(т.е. имат еднакви dnaCode
), тогава нашият метод връща същото число. Нека разгледаме един по-сложен пример. Да предположим, че нашата програма трябва да избере луксозни автомобor за колекционери на коли. Колекционерството може да бъде сложно хоби с много особености. Конкретен автомобил от 1963 г. може да струва 100 пъти повече от автомобил от 1964 г. Червен автомобил от 1970 г. може да струва 100 пъти повече от син автомобил от същата марка от същата година. 
Person
класа, ние отхвърлихме повечето от полетата (т.е. човешки характеристики) като незначителни и използвахме самоdnaCode
поле в сравненията. Сега работим в една много идиосинкратична сфера, в която няма незначителни детайли! Ето нашия LuxuryAuto
клас:
public class LuxuryAuto {
private String model;
private int manufactureYear;
private int dollarPrice;
public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
this.model = model;
this.manufactureYear = manufactureYear;
this.dollarPrice = dollarPrice;
}
// ...getters, setters, etc.
}
Сега трябва да разгледаме всички полета в нашите сравнения. Всяка грешка може да струва на клиента стотици хиляди долари, така че би било по-добре да сте прекалено безопасни:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LuxuryAuto that = (LuxuryAuto) o;
if (manufactureYear != that.manufactureYear) return false;
if (dollarPrice != that.dollarPrice) return false;
return model.equals(that.model);
}
В нашия equals()
метод не сме забравor всички проверки, за които говорихме по-рано. Но сега сравняваме всяко от трите полета на нашите обекти. За тази програма се нуждаем от абсолютно equalsство, т.е. equalsство на всяко поле. hashCode
Howво за
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = result + manufactureYear;
result = result + dollarPrice;
return result;
}
Полето model
в нашия клас е String. Това е удобно, защото String
класът вече замества hashCode()
метода. Изчисляваме model
хеш codeа на полето и след това добавяме сумата от другите две числови полета към него. Разработчиците на Java имат прост трик, който използват, за да намалят броя на сблъсъците: когато изчислявате хеш code, умножете междинния резултат по нечетно просто число. Най-често използваното число е 29 or 31. Сега няма да се задълбочаваме в математическите тънкости, но в бъдеще не забравяйте, че умножаването на междинните резултати с достатъчно голямо нечетно число помага да се "разпространят" резултатите от хеш функцията и, следователно, намалете броя на обектите със същия хеш code. За нашия hashCode()
метод в LuxuryAuto ще изглежда така:
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
Можете да прочетете повече за всички тънкости на този механизъм в тази публикация в StackOverflow , Howто и в книгата Effective Java от Joshua Bloch. И накрая, още един важен момент, който си струва да се спомене. Всеки път, когато отменяхме метода equals()
and hashCode()
, избирахме определени полета на екземпляр, които се вземат предвид в тези методи. Тези методи разглеждат едни и същи полета. Но можем ли да разгледаме различни полета в equals()
и hashCode()
? Технически можем. Но това е лоша идея и ето защо:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LuxuryAuto that = (LuxuryAuto) o;
if (manufactureYear != that.manufactureYear) return false;
return dollarPrice == that.dollarPrice;
}
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
Ето нашите equals()
и hashCode()
методи за LuxuryAuto
класа. Методът hashCode()
остана непроменен, но премахнахме model
полето от equals()
метода. Моделът вече не е характеристика, използвана, когато equals()
методът сравнява два обекта. Но при изчисляването на хеш codeа това поле все още се взема предвид. Какво получаваме в резултат? Нека създадем две коли и да разберем!
public class Main {
public static void main(String[] args) {
LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);
System.out.println("Are these two objects equal to each other?");
System.out.println(ferrariGTO.equals(ferrariSpider));
System.out.println("What are their hash codes?");
System.out.println(ferrariGTO.hashCode());
System.out.println(ferrariSpider.hashCode());
}
}
Are these two objects equal to each other?
true
What are their hash codes?
-1372326051
1668702472
грешка! Използвайки различни полета за equals()
и hashCode()
методи, ние нарушихме договорите, които са установени за тях! Два обекта, които са еднакви според equals()
метода, трябва да имат еднакъв хеш code. За тях получихме различни стойности. Такива грешки могат да доведат до абсолютно невероятни последици, особено при работа с колекции, които използват хеш. В резултат на това, когато замените equals()
и hashCode()
, трябва да имате предвид същите полета. Този урок беше доста дълъг, но днес научихте много! :) Сега е време да се върнем към решаването на задачи!
GO TO FULL VERSION