CodeGym /Java блог /Случаен /методи equals и hashCode: най-добри практики
John Squirrels
Ниво
San Francisco

методи equals и hashCode: най-добри практики

Публикувано в групата
здрасти Днес ще говорим за два важни метода в Java: equals()и hashCode(). Това не е първият път, когато ги срещаме: курсът CodeGym започва с кратък урок за equals()— прочетете го, ако сте го забравor or не сте го виждали преди... методи equals и hashCode: най-добри практики - 1В днешния урок ще говорим за тези концепции в детайли. И повярвайте ми, имаме за Howво да си говорим! Но преди да преминем към новото, нека опресним това, което вече разгледахме :) Както си спомняте, обикновено е лоша идея да сравнявате два обекта с помощта на ==оператора, защото ==сравнява препратки. Ето нашия пример с автомоб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 и hashCode: най-добри практики - 2Разбира се, само ДНК тест може да даде гаранция. Двама души могат да имат еднакъв цвят на очите, прическа, нос и дори белези — има много хора на света и е невъзможно да се гарантира, че няма двойници. Но имаме нужда от надежден механизъм: само резултатът от ДНК тест ще ни позволи да направим точно заключение. Какво означава това за нашия 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()метода, не забравяйте да спазвате следните изисквания:
  1. Рефлексивност.

    Когато equals()методът се използва за сравняване на обект със себе си, той трябва да върне true.
    Вече сме изпълнor това изискване. Нашият метод включва:

    
    if (this == o) return true;
    

  2. Симетрия.

    Ако a.equals(b) == true, тогава b.equals(a)трябва да се върне true.
    Нашият метод отговаря и на това изискване.

  3. Преходност.

    Ако два обекта са равни на няHowъв трети обект, тогава те трябва да са равни един на друг.
    Ако a.equals(b) == trueи a.equals(c) == true, тогава b.equals(c)също трябва да върне true.

  4. Упоритост.

    Резултатът от equals()трябва да се променя само когато участващите полета се променят. Ако данните на двата обекта не се променят, тогава резултатът equals()трябва винаги да е един и същ.

  5. Не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:
  1. Ако два обекта са еднакви (т.е. equals()методът връща true), тогава те трябва да имат еднакъв хеш code.

    В противен случай нашите методи биха бor безсмислени. Както споменахме по-горе, hashCode()първо трябва да се извърши проверка, за да се подобри производителността. Ако хеш codeовете бяха различни, тогава проверката щеше да върне false, въпреки че обектите всъщност са равни според начина, по който сме дефинирали метода equals().

  2. Ако hashCode()методът се извика няколко пъти на един и същи обект, той трябва да връща едно и също число всеки път.

  3. Правило 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 пъти повече от син автомобил от същата марка от същата година. методи equals и hashCode: най-добри практики - 4В предишния ни пример, с 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ство на всяко поле. hashCodeHowво за

@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(), трябва да имате предвид същите полета. Този урок беше доста дълъг, но днес научихте много! :) Сега е време да се върнем към решаването на задачи!
Коментари
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION