JavaRush /Курсы /All lectures for GE purposes /მეთოდები equals & hashCode: რატომ, სად გამოიყენება და...

მეთოდები equals & hashCode: რატომ, სად გამოიყენება და როგორ მუშაობს

All lectures for GE purposes
1 уровень , 192 лекция
Открыта

— ახლა მოგიყვები არანაკლებ საჭირო მეთოდებზე equals(Object o) & hashCode().

როგორც ალბათ უკვე გაგახსენდა, 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 მიზანი არის განსაზღვროს ობიექტები შიგნით იდენტურია თუ არა, მათი შიდა კონტენტის შედარებით.

— და როგორ აკეთებს ამას?

— აქ ყველაფერი მსგავსია მეთოდ toString()-ის.

კლას Object-ს აქვს თავისი რეალიზაცია equals მეთოდის, რომელიც უბრალოდ ადარებს ბმულებს:

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

— მ-და. იმასთან რაც ებრძოდა, იმაზე ნატკენი.

— არ დაკარგო იმედი. აქაც ყველაფერი ძალიან საეჭვოა.

ეს მეთოდი შეიქმნა, რათა დეველოპერები მის თავიანთ კლასებში გადაეწერათ. მხოლოდ კლასის დეველოპერმა იცის, რა მონაცემები არის მნიშვნელოვანი, რა უნდა გაითვალისწინოს შედარებისას და რა – არა.

— შეიძლება მაგალითი ასეთი მეთოდის?

— რა თქმა უნდა. მივიღოთ კლასი, რომელიც აღწერს მათემატიკურ ფრაქციებს, ის გამოიყურებოდა ასე (სიწმინდისთვის, მე გადავთარგმნი ინგლისურ სახელებს ქართულ ენაზე):

მაგალითი
class დრობი {
    private int ჩასვლელი;
    private int ზემენაკთარი;

    დრობი(int ჩასვლელი, int ზემენაკთარი) {
        this.ჩასვლელი = ჩასვლელი;
        this.ზემენაკთარი = ზემენაკთარი;
    }

    public boolean equals(Object obj) {
        if (obj == null)
            return false;

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

        დრობი other = (დრობი) obj;
        return this.ჩასვლელი * other.ზემენაკთარი == this.ზემენაკთარი * other.ჩასვლელი;
    }
}
მოძახების მაგალითი:
დრობი ერთი = new დრობი(2,3);
დრობი ორი = new დრობი(4,6);
System.out.println(ერთი.equals(ორი));
მოძახების შედეგი იქნება true.
დრობი 2/3 ტოლია დრობი 4/6

— მეტი სიწმინდისთვის მე გამოვიყენე ქართული სახელები. ეს მხოლოდ სწავლების მიზნებისთვისაა შესაძლებელი.

ახლა მაგალითს გავარჩევთ.

ჩვენ გადავშალეთ მეთოდი equals, და ახლა კლასის დრობი ობიექტებისთვის მას ექნება თავისი რეალიზაცია.

ამ მეთოდში არის რამდენიმე შემოწმება:

1) თუ შედარებისთვის გადაცემული ობიექტი არის null, მაშინ ობიექტები არ არის თოლი. ობიექტი, რომლისთვისაც გამოვიძახეთ მეთოდი equals ზუსტად არ იქნება null.

2) კლასებზე შედარების შემოწმება. თუ ობიექტები სხვადასხვა კლასისაა, მაშინ ჩვენ არ ვცდილობთ მათ შედარებას, მაშინვე ვიტყვით, რომ ეს სხვადასხვა ობიექტებია – return false.

3) სკოლის მეორე კლასიდან ყველამ ახსოვს, რომ დრობი 2/3 ტოლია დრობი 4/6. როგორ შევამოწმოთ ეს?

2/3 == 4/6
ორივე მხარეს გავამრავლოთ ორივე გაყოფილით (6 და 3), მივიღებთ:
6 * 2 == 4 * 3
12 == 12
ზოგადი წესი:
თუ
a / b == c / d
მაშინ
a * d == c * b

— ამიტომ მეთოდის equals მესამე ნაწილში ჩვენ გადავიყვანთ გადაცემულ ობიექტს ტიპზე დრობი და ვადარებთ დრობებს.

— გასაგებია. თუ უბრალოდ შევადარებდით ჩასვლელთან ჩასვლელს და ზემენაკთართან ზემენაკთარს, მაშინ დრობი 2/3 არ იქნებოდა ტოლი 4/6-ს.

ახლა გასაგებია, რას გულისხმობდი, როდესაც ამბობდი, რომ მხოლოდ კლასის დეველოპერმა იცის, როგორ სწორად შეადაროს იგი.

— კი, მაგრამ ეს მხოლოდ ნახევარია. კიდევ ერთი მეთოდი არის – hashCode()

— equals მეთოდის გასაგებია, მაგრამ რატომ გვჭირდება hashCode()?

— მეთოდი hashCode გჭირდებათ სწრაფი შედარებისთვის.

მეთოდს equals აქვს დიდი მარცხი – ის ძალიან ნელა მუშაობს. დავუშვათ, თქვენ გაქვთ სიმრავლე (Set) მილიონი ელემენტით, და უნდა შეამოწმოთ, შეიცავს თუ არა იგი გარკვეულ ობიექტს. როგორ გავაკეთოთ ეს?

— შეიძლება ციკლში გავიაროთ ყველა ელემენტი და შევადაროთ საჭირო ობიექტი ყოველ სიმრავლეს ობიექტთან. სანამ არ ვიპოვით საჭირო.

— და თუ ის იქ არ არის? ჩვენ გავაკეთებთ მილიონ შედარებას, რათა ვიცოდეთ, რომ იქ არ არის ეს ობიექტი? არაა მეტისმეტი?

— კი, ჩემთვისაც გასაგებია, რომ ძალიან ბევრი შედარებაა. სხვა გზა არსებობს?

— კი, ამისთვის გამოიყენება hashCode().

მეთოდი hashCode() ყოველი ობიექტისთვის აბრუნებს გარკვეულ რიცხვს. როგორ – ესაც თავად დეველოპერმა უნდა გადაწყვიტოს, როგორც equals მეთოდის შემთხვევაში.

მოდით ვითარება მაგალითზე განვიხილოთ:

წარმოიდგინე, რომ გაქვს მილიონი 10-ნიშანიანი რიცხვები. მაშინ ყოველი რიცხვის hashCode შეიძლება აირჩეს 100-ზე გაყოფის ნაშთი.

მაგალითი:

რიცხვი ჩვენი hashCode
1234567890 90
9876554321 21
9876554221 21
9886554121 21

— კი, ეს გასაგებია. და რა ვქნათ ამ hashCode-რიცხვთან?

— რიცხვების შედარების ნაცვლად, ჩვენ შევადარებთ მათ hashCode. ასე უფრო სწრაფია.

და მხოლოდ თუ hashCode-ები ტოლია, შედარება ხორციელდება equals მიხედვით.

— კი, ასე უფრო სწრაფია. მაგრამ მაინც მოგვიწევს მილიონი შედარება, მხოლოდ უფრო მცირე რიცხვების, და იმ რიცხვებისთვის, რომელთა hashCode ტოლია, კვლავ გამოვიძახებთ equals.

— არა, შესაძლებელი იქნება ბევრად ნაკლები რაოდენობის გამოყენებით.

წარმოიდგინე, რომ ჩვენი სიმრავლე ინახავს რიცხვებს, რომლებიც დაჯგუფებულია hashCode ან დალაგებულია hashCode მიხედვით (რაც მათი დაჯგუფების ტოლფასია, რადგან რიცხვები ერთნაირი hashCode-ით მდებარეობენ ახლოს). მაშინ შესაძლებელია ძალიან სწრაფად და მარტივად მოვაცილოთ გამოუსადეგარი ჯგუფები, საკმარისია თითოეული ჯგუფისთვის ერთხელ შევამოწმოთ, ემთხვევა თუ არა მის hashCode მოცემულ ობიექტის hashCode-ს.

წარმოიდგინე, რომ ხარ სტუდენტი და ეძებ შენ მეგობარს, რომელსაც ვიცნობ სახეზე და რომელზეც ცნობილია, რომ ის ცხოვრობს 17 საერთო საცხოვრებელში. მაშინ უბრალოდ გადიხარ ყველა უნივერსიტეტის საერთო საცხოვრებელში და თითოეულში კითხულობ «ეს 17 საერთო საცხოვრებელია?». თუ არა, მაშინ აცვიათ ამ საერთო საცხოვრებელიდან ყველა და გადადიხარ შემდეგზე. თუ «კი», მაშინ იწყებ ყველა ოთახში სიარულს და ეძებ მეგობარს.

ამ მაგალითში საერთო საცხოვრებლის ნომერი – 17 – არის hashCode.

დეველოპერმა, რომელიც hashCode ფუნქციას ახორციელებს, უნდა იცოდეს შემდეგი რამეები:

ა) ორმოცდამეათე ობიექტს შეუძლია ჩანდეს იგივე hashCode (სხვადასხვა ადამიანი შეიძლება ცხოვრობდეს ერთ საერთო საცხოვრებელში)

ბ) ერთნაირი ობიექტებით (equals-ის თვალსაზრისითუნდა იყოს ერთნაირი hashCode.

გ) ჰეშ-კოდები უნდა იქნას შერჩეული ისე, რომ არ იყოს დიდი რაოდენობის სხვადასხვა ობიექტები ერთნაირი hashCode-ით. ეს ყველა მათი უპირატესობას გაანეიტრალებს (შენ მიხვედი 17 საერთო საცხოვრებელში, ხოლო იქ ნახევარი უნივერსიტეტი ცხოვრობს. გაბედეთ).

და ახლა ყველაზე მნიშვნელოვანი. თუ გადაშლით მეთოდს equals, აუცილებლად უნდა გადაშალოთ მეთოდი hashCode(), ზემოთ ხსენებული სამი წესის გათვალისწინებით.

საქმე იმაშია, რომ კოლექციები Java-ში, ვიდრე მათ შედარება მოახდენენ equals საშუალებით, ყოველთვის ეძებენ/ჰეშ-კოდებით ადარებენ მათ hashCode მეთოდის მეშვეობით. და თუ ერთნაირი ობიექტების ეყოლებათ სხვადასხვა hashCode, მაშინ ობიექტები მიიჩნევიან სხვადასხვა - შედარების მომენტამდე equals მეთოდის საშუალებით ვერ მივადგენთ.

ჩვენს მაგალითში დრობიზე, თუ ავიღებდით hashCode-ს, რომელიც ტოლი ჩასვლელთან, მაშინ დრობები 2/3 და 4/6 ექნებოდათ სხვადასხვა hashCode. დრობები – ერთი და იგივეა, equals ამბობს, რომ ისინი ერთნაირია, მაგრამ hashCode ამბობს, რომ ისინი არ არიან ერთნაირი. და თუ შედარების მომენტამდე equals მეთოდის გამოყენებისას შევადარებთ hashCode-ით, მივიღებთ, რომ ობიექტები არ არიან ერთნაირი და compareTo-ის ვიზიტი არ იქენება.

მაგალითი:

HashSet<დრობი>set = new HashSet<დრობი>();
set.add(new დრობი(2,3));System.out.println( set.contains(new დრობი(4,6)) );
თუ მეთოდი hashCode() დააბრუნებს დრობიის ჩასვლელს, მაშინ შედეგი იქნება false.
ობიექტი new დრობი(4,6) არ ჩამოვა კოლექციაში.

— და როგორ სწორად განვახორციელო hashCode დრობისთვის?

— აქ უნდა გვახსოვდეს, რომ ერთნაირ დრობებთან აუცილებლად უნდა იყოს ერთნაირი hashCode.

ვარიანტი 1: hashCode ტოლია მთელი რიცხვისგან გაყოფის შედეგად.

დრობი 7/5 და 6/5 ეს იქნება 1.

დრობი 4/5 და 3/5 ეს იქნება 0.

მაგრამ ეს ვარიანტი ცუდია შედარებისთვის დრობებისთვის, რომლებიც ნამდვილად ნაკლებია 1. მთელი ნაწილი (hashCode) ყოველთვის იქნება 0.

ვარიანტი 2: hashCode ტოლია მთელი რიცხვისგან გასაყოფი ნომრისგან გადათვლის შედეგად.

ეს ვარიანტი უპრობლემოდ იქნება იმ შემთხვევისთვის, როცა დრობი ნაკლებია 1-ზე. თუ დრობი ნაკლებია 1, მაშინ გადატრიალებული დრობი უფრო მეტია 1-ზე. და თუ ყველა დრობი გადავახდრებთ – ეს არანაირად არ იმოქმედებს მათ შედარებაზე.

დასკვნითი ვარიანტი ააგებს ორივე გადაწყვეტილებას:

public int hashCode() {
    return ჩასვლელი/ზემენაკთარი + ზემენაკთარი/ჩასვლელი;
}

შევამოწმოთ დრობები 2/3 და 4/6. მათ უნდა ჰქონდეთ ერთნაირი hashCode-ი:

დრობი 2/3 დრობი 4/6
ჩასვლელი / ზემენაკთარი 2 / 3 == 0 4 / 6 == 0
ზემენაკთარი / ჩასვლელი 3 / 2 == 1 6 / 4 == 1
ჩასვლელი / ზემენაკთარი
+
ზემენაკთარი / ჩასვლელი
0 + 1 == 1 0 + 1 == 1

ამის დასრულება – ეს არის ყველაფერი.

— მადლობა, ელლი, ეს მართლაც საინტერესო იყო.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ