CodeGym /Java курс /Модул 2: Java Core /Методите equals & hashCode: защо и къде да ги използваме ...

Методите equals & hashCode: защо и къде да ги използваме и как работят

Модул 2: Java Core
Ниво , Урок
На разположение

„Сега ще ви разкажа за някои методи, които са също толкова полезни:  equals(Object o) & hashCode() .“

„Вероятно вече сте си спомнor, че в Java при сравняване на референтни променливи не се сравняват самите обекти, а по-скоро референциите към обектите.“

Код Обяснение
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i==j);
i не е равно на j
Променливите сочат към различни обекти.
Въпреки че обектите съдържат едни и същи данни.
Integer i = new Integer(1);
Integer j = i;
System.out.println(i==j);
i е равно на j. Променливите съдържат препратка към един и същи обект.

— Да, помня това.

Равните  . _

„Методът equals е стандартното решение тук. Целта на метода equals е да определи дали обектите са вътрешно идентични чрез сравняване на това, което се съхранява вътре в тях.“

— И How става това?

„Всичко е много подобно на метода toString().“

Класът Object има собствена реализация на метода equals, който просто сравнява препратките:

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

„Страхотно... пак се връщаме към това, нали?“

"Дръжте брадичката си вдигната! Всъщност е много сложно."

„Този ​​метод е създаден, за да позволи на разработчиците да го презапишат в собствените си класове. В края на краищата, само разработчикът на клас знае кои данни са подходящи и кои не, когато се сравняват.“

„Можете ли да дадете пример?“

„Разбира се. Да предположим, че имаме клас, който представлява математически дроби. Ще изглежда така:“

Пример:
class Fraction
{
private int numerator;
private int denominator;
Fraction(int numerator, int denominator)
{
this.numerator  = numerator;
this.denominator = denominator;
}public boolean equals(Object obj)
{
if (obj==null)
return false;

if (obj.getClass() != this.getClass() )
return false;

Fraction other = (Fraction) obj;
return this.numerator* other.denominator == this.denominator * other.numerator;
}
}
Примерно извикване на метод:
Fraction one = new Fraction(2,3);
Fraction two = new Fraction(4,6);
System.out.println(one.equals(two));
Извикването на метода ще върне true.
Дробта 2/3 е равна на дробта 4/6

„А сега нека анализираме този пример.“

„Ние пренебрегнахме метода equals , така че обектите Fraction ще имат своя собствена реализация.

„Има няколко проверки в метода:“

" 1)  Ако обектът, предаден за сравнение, е null , тогава обектите не са равни. Ако можете да извикате метода equals на обект, тогава той определено не е null ."

" 2)  Сравнение на класове. Ако обектите са екземпляри от различни класове, тогава няма да се опитваме да ги сравняваме. Вместо това веднага ще използваме return false , за да посочим, че това са различни обекти."

" 3)  Всички помнят от втори клас, че 2/3 е равно на 4/6. Но How да проверите това?"

2/3 == 4/6
Умножаваме двете страни по двата делителя (6 и 3) и получаваме:
6 * 2 == 4 * 3
12 == 12
Общо правило:
Ако
a / b == c / d
Тогава
a * d == c * b

„Съответно, в третата част на метода equals , ние прехвърляме подавания обект към Fraction и сравняваме дробите.“

„Разбрах. Ако просто сравним числителя с числителя и знаменателя със знаменателя, тогава 2/3 не е равно на 4/6.“

„Сега разбирам Howво имахте предвид, когато казахте, че само разработчикът на даден клас знае How да го сравни правилно.“

„Да, но това е само половината от историята.  Има друг метод: hashCode().

„Всичко за метода equals вече има смисъл, но защо се нуждаем от  hashCode ()?

„ Методът hashCode е необходим за бързи сравнения.“

„ Методът equals има основен недостатък: работи твърде бавно. Да предположим, че имате набор от мorони елементи и трябва да проверите дали съдържа конкретен обект. Как го правите?“

„Мога да преминавам през всички елементи с помощта на цикъл и да сравня обекта с всеки обект в комплекта. Докато намеря съвпадение.“

„И ако не е там? Бихме извършor мorон сравнения, само за да разберем, че обектът не е там? Това не изглежда ли много?“

„Да, дори аз осъзнавам, че това са твърде много сравнения. Има ли друг начин?“

„Да, можете да използвате hashCode () за това.

Методът hashCode () връща конкретно число за всеки обект. Разработчикът на даден клас решава Howво число да се върне, точно Howто той or тя прави за метода equals.

„Нека да разгледаме един пример:“

„Представете си, че имате мorон 10-цифрени числа. Тогава можете да накарате хеш-codeа на всяко число да бъде остатъкът след разделяне на числото на 100.“

Ето един пример:

Номер Нашият хешcode
1234567890 90
9876554321 21
9876554221 21
9886554121 21

„Да, това има смисъл. И Howво да правим с този хешcode?“

„Вместо да сравняваме числата, ние сравняваме техните хешcodeове . Така е по-бързо.“

„И ние извикваме равни само ако техните хешcodeове са равни.“

„Да, това е по-бързо. Но все още трябва да направим мorони сравнения. Ние просто сравняваме по-малки числа и все още трябва да извикаме равни за всички числа със съвпадащи хешcodeове.“

„Не, можете да се измъкнете с много по-малък брой сравнения.“

„Представете си, че нашият набор съхранява числа, групирани or сортирани по hashCode (сортирането им по този начин е по същество групирането им, тъй като числата с еднакъв hashCode ще бъдат едно до друго). Тогава можете много бързо и лесно да отхвърлите неподходящи групи. Това е достатъчно за да проверите веднъж на група, за да видите дали нейният hashCode съвпада с hashCode на обекта."

„Представете си, че сте студент, който търси приятел, когото можете да разпознаете по зрението и за когото знаем, че живее в общежитие 17. След това просто отивате във всяко общежитие в университета и питате „Това общежитие 17 ли е?“ Ако не е, тогава игнорирате всички в общежитието и преминавате към следващото. Ако отговорът е „да“, тогава започвате да минавате покрай всяка от стаите, търсейки своя приятел.“

„В този пример номерът на общежитието (17) е hashCode.“

„Програмистът, който внедрява функция hashCode, трябва да знае следното:“

A)  два различни обекта могат да имат един и същ хешcode  (различни хора могат да живеят в едно общежитие)

B)  обекти, които са еднакви  ( според метода equals ),  трябва да имат един и същ hashCode. .

В)  хеш-codeовете трябва да бъдат избрани така, че да няма много различни обекти с един и същ хеш-code.  Ако има, тогава потенциалните предимства на хешcodeовете се губят (стигате до общежитие 17 и установявате, че половината университет живее там. Гадно!).

„А сега най-важното. Ако замените метода equals , вие абсолютно трябва да замените метода hashCode () и да спазите трите правила, описани по-горе.

„Причината е следната: в Java обектите в колекция винаги се сравняват/извличат с помощта на hashCode(), преди да бъдат сравнявани/извличани с помощта на equals.  И ако идентични обекти имат различни hashCodes, тогава обектите ще се считат за различни и методът equals дори няма да бъде призован.

„В нашия пример с фракция, ако направим хеш-codeа equals на числителя, фракциите 2/3 и 4/6 ще имат различни хеш-codeове. Фракциите са еднакви и методът equals казва, че са еднакви, но техните хеш-codeове казват те са различни. И ако сравним с помощта на hashCode, преди да сравним с помощта на equals, тогава стигаме до заключението, че обектите са различни и никога дори не стигаме до метода equals."

Ето един пример:

HashSet<Fraction>set = new HashSet<Fraction>();
set.add(new Fraction(2,3));System.out.println( set.contains(new Fraction(4,6)) );
Ако методът hashCode()  върне числителя на дробите, резултатът ще бъде  false .
И обектът «new Fraction(4,6) » няма да бъде намерен в колекцията.

„И така, Howъв е правилният начин за внедряване на hashCode за дроби?“

„Тук трябва да запомните, че еквивалентните дроби трябва да имат един и същ хешcode.“

" Версия 1 : hashCode е equals на резултата от целочислено деление."

„За 7/5 и 6/5 това ще бъде 1.“

„За 4/5 и 3/5 това ще бъде 0.“

„Но тази опция не е подходяща за сравняване на дроби, които умишлено са по-малки от 1. Хеш-codeът (резултат от целочислено деление) винаги ще бъде 0.“

" Версия 2 : hashCode е equals на резултата от целочисленото деление на знаменателя на числителя."

„Тази опция е подходяща за случаи, когато дробта е по-малка от 1. Ако дробта е по-малка от 1, тогава нейната обратна е по-голяма от 1. И ако обърнем всички дроби, тогава сравненията по ниHowъв начин не са засегнати.“

„Нашата окончателна version съчетава и двете решения:“

public int hashCode()
{
return numerator/denominator + denominator/numerator;
}

Нека го тестваме с 2/3 и 4/6. Те трябва да имат идентични хеш codeове:

Дроб 2/3 Дроб 4/6
числител / знаменател 2/3 == 0 4/6 == 0
знаменател / числител 3/2 == 1 6/4 == 1
числител / знаменател
+
знаменател / числител
0 + 1 == 1 0 + 1 == 1

"Това е всичко за сега."

„Благодаря, Ели. Беше наистина интересно.“

Коментари
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION