JavaRush /Blog Java /Random-PL /Równa się i kontrakty hashCode lub cokolwiek to jest
Aleksandr Zimin
Poziom 1
Санкт-Петербург

Równa się i kontrakty hashCode lub cokolwiek to jest

Opublikowano w grupie Random-PL
Zdecydowana większość programistów Java wie oczywiście, że metody equalshashCodeze sobą ściśle powiązane i że wskazane jest konsekwentne zastępowanie obu metod w swoich klasach. Nieco mniejsza liczba wie, dlaczego tak się dzieje i jakie smutne konsekwencje mogą wyniknąć w przypadku złamania tej zasady. Proponuję rozważyć koncepcję tych metod, powtórzyć ich cel i zrozumieć, dlaczego są tak powiązane. Ten artykuł, podobnie jak poprzedni o ładowaniu klas, napisałem dla siebie, aby w końcu ujawnić wszystkie szczegóły problemu i nie wracać już do zewnętrznych źródeł. Dlatego chętnie przyjmę konstruktywną krytykę, bo jeśli gdzieś są luki, to należy je wyeliminować. Artykuł, niestety, okazał się dość długi.

równa się regułom zastępowania

W Javie wymagana jest metoda equals()potwierdzająca lub zaprzeczająca faktowi, że dwa obiekty tego samego pochodzenia są logicznie równe . Oznacza to, że porównując dwa obiekty programista musi zrozumieć, czy ich pola znaczące są równoważne . Nie jest konieczne, aby wszystkie pola były identyczne, ponieważ metoda equals()zakłada logiczną równość . Ale czasami nie ma szczególnej potrzeby stosowania tej metody. Jak mówią, najłatwiejszym sposobem uniknięcia problemów przy obsłudze konkretnego mechanizmu jest jego nieużywanie. Należy również zauważyć, że po zerwaniu umowy equalstracisz kontrolę nad zrozumieniem, w jaki sposób inne obiekty i struktury będą oddziaływać na Twój obiekt. Później znalezienie przyczyny błędu będzie bardzo trudne.

Kiedy nie zastępować tej metody

  • Kiedy każde wystąpienie klasy jest unikalne.
  • W większym stopniu dotyczy to tych klas, które zapewniają określone zachowanie, a nie są przeznaczone do pracy z danymi. Takie jak na przykład klasa Thread. Dla nich equalsimplementacja metody dostarczonej przez klasę Objectjest więcej niż wystarczająca. Innym przykładem są klasy wyliczeniowe ( Enum).
  • Kiedy w rzeczywistości klasa nie jest wymagana do określenia równoważności jej instancji.
  • Na przykład w przypadku klasy java.util.Randomnie ma w ogóle potrzeby porównywać ze sobą instancji tej klasy, sprawdzając, czy mogą one zwrócić tę samą sekwencję liczb losowych. Po prostu dlatego, że natura tej klasy nawet nie implikuje takiego zachowania.
  • Kiedy rozszerzana klasa ma już własną implementację metody equalsi zachowanie tej implementacji Ci odpowiada.
  • Na przykład dla klas , Setimplementacja odbywa się odpowiednio w , i . ListMapequalsAbstractSetAbstractListAbstractMap
  • I wreszcie, nie ma potrzeby zastępowania, equalsgdy zakres twojej klasy to privatelub package-privatei masz pewność, że ta metoda nigdy nie zostanie wywołana.

równa się kontraktowi

Nadpisując metodę, equalsprogramista musi przestrzegać podstawowych zasad określonych w specyfikacji języka Java.
  • Refleksyjność
  • dla dowolnej wartości xwyrażenie x.equals(x)musi zwrócić true.
    Biorąc pod uwagę - czyli takie, żex != null
  • Symetria
  • dla dowolnych podanych wartości xi y, x.equals(y)powinien zwrócić truetylko wtedy, gdy y.equals(x)zwróci true.
  • Przechodniość
  • dla dowolnych podanych wartości i x, jeśli zwraca i zwraca , musi zwrócić wartość . yzx.equals(y)truey.equals(z)truex.equals(z)true
  • Konsystencja
  • dla dowolnych podanych wartości, xa ypowtórzone wywołanie x.equals(y)zwróci wartość poprzedniego wywołania tej metody, pod warunkiem, że pola użyte do porównania obu obiektów nie uległy zmianie pomiędzy wywołaniami.
  • Porównanie zerowe
  • dla dowolnej wartości xwywołanie x.equals(null)musi zwrócić false.

jest równoznaczne z naruszeniem umowy

Wiele klas, na przykład te z Java Collections Framework, zależy od implementacji metody equals(), więc nie powinieneś jej zaniedbywać, ponieważ Naruszenie umowy tej metody może prowadzić do irracjonalnego działania aplikacji i w tym przypadku znalezienie przyczyny będzie dość trudne. Zgodnie z zasadą zwrotności każdy przedmiot musi być sobie równoważny. W przypadku naruszenia tej zasady, dodając obiekt do kolekcji, a następnie szukając go metodą, contains()nie uda nam się znaleźć obiektu, który właśnie dodaliśmy do kolekcji. Warunek symetrii stwierdza, że ​​dowolne dwa obiekty muszą być równe, niezależnie od kolejności, w jakiej są porównywane. Na przykład, jeśli masz klasę zawierającą tylko jedno pole typu string, niepoprawne będzie porównywanie equalstego pola z ciągiem znaków w metodzie. Ponieważ w przypadku porównania odwrotnego metoda zawsze zwróci wartość false.
// Нарушение симметричности
public class SomeStringify {
    private String s;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof SomeStringify) {
            return s.equals(((SomeStringify) o).s);
        }
        // нарушение симметричности, классы разного происхождения
        if (o instanceof String) {
            return s.equals(o);
        }
        return false;
    }
}
//Правильное определение метода equals
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    return o instanceof SomeStringify &&
            ((SomeStringify) o).s.equals(s);
}
Z warunku przechodniości wynika, że ​​jeśli dowolne dwa z trzech obiektów są równe, to w tym przypadku wszystkie trzy muszą być równe. Zasadę tę można łatwo naruszyć, gdy konieczne jest rozszerzenie pewnej klasy bazowej poprzez dodanie do niej znaczącego komponentu . Na przykład do klasy Pointze współrzędnymi xi ytrzeba dodać kolor punktu poprzez jego rozwinięcie. W tym celu należy zadeklarować klasę ColorPointz odpowiednim polem color. Zatem jeśli w klasie rozszerzonej wywołamy equalsmetodę nadrzędną, a w rodzicielskiej założymy, że porównywane są tylko współrzędne xi y, to dwa punkty o różnych kolorach, ale o tych samych współrzędnych, zostaną uznane za równe, co jest błędne. W takim przypadku konieczne jest nauczenie klasy pochodnej rozróżniania kolorów. Aby to zrobić, możesz użyć dwóch metod. Ale jedno naruszy zasadę symetrii , a drugie - przechodniości .
// Первый способ, нарушая симметричность
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint)) return false;
    return super.equals(o) && ((ColorPoint) o).color == color;
}
W tym przypadku wywołanie point.equals(colorPoint)zwróci wartość true, a porównanie colorPoint.equals(point)zwróci false, ponieważ oczekuje obiektu „swojej” klasy. W ten sposób naruszona zostaje zasada symetrii. Druga metoda polega na sprawdzeniu „w ciemno” w przypadku, gdy nie ma danych o kolorze punktu, czyli mamy klasę Point. Lub sprawdź kolor, jeśli są dostępne informacje na jego temat, czyli porównaj obiekt danej klasy ColorPoint.
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof Point)) return false;

    // Слепая проверка
    if (!(o instanceof ColorPoint))
        return super.equals(o);

    // Полная проверка, включая цвет точки
    return super.equals(o) && ((ColorPoint) o).color == color;
}
Zasada przechodniości zostaje tu naruszona w następujący sposób. Załóżmy, że istnieje definicja następujących obiektów:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
Zatem, chociaż równość p1.equals(p2)i jest spełniona p2.equals(p3), p1.equals(p3)zwróci wartość false. Jednocześnie druga metoda moim zdaniem wygląda mniej atrakcyjnie, ponieważ W niektórych przypadkach algorytm może zostać oślepiony i nie przeprowadzić pełnego porównania, a Ty możesz o tym nie wiedzieć. Trochę poezji Generalnie, jak rozumiem, nie ma konkretnego rozwiązania tego problemu. Istnieje opinia autorytatywnej autorki o nazwisku Kay Horstmann, że użycie operatora można zastąpić instanceofwywołaniem metody getClass()zwracającej klasę obiektu i zanim zaczniesz porównywać same obiekty, upewnij się, że są tego samego typu , i nie zwracaj uwagi na fakt ich wspólnego pochodzenia. Zatem zasady symetrii i przechodniości zostaną spełnione. Ale jednocześnie po drugiej stronie barykady stoi inny autor, nie mniej szanowany w szerokich kręgach, Joshua Bloch, który uważa, że ​​takie podejście narusza zasadę substytucji Barbary Liskov. Zasada ta stanowi, że „kod wywołujący musi traktować klasę bazową w taki sam sposób, jak jej podklasy, nawet o tym nie wiedząc ” . A w rozwiązaniu zaproponowanym przez Horstmanna zasada ta jest wyraźnie naruszona, ponieważ zależy to od wdrożenia. Krótko mówiąc, jasne jest, że sprawa jest ciemna. Warto też zauważyć, że Horstmann wyjaśnia zasadę stosowania swojego podejścia i pisze prostym językiem, że przy projektowaniu klas trzeba zdecydować się na strategię, a jeśli testy równości będą przeprowadzane tylko przez nadklasę, można to zrobić wykonując operacja instanceof. W przeciwnym wypadku, gdy semantyka sprawdzenia zmienia się w zależności od klasy pochodnej i konieczne jest przesunięcie implementacji metody w dół hierarchii, należy zastosować metodę getClass(). Joshua Bloch proponuje z kolei porzucenie dziedziczenia i wykorzystanie kompozycji obiektów poprzez włączenie do ColorPointklasy klasy Pointi zapewnienie metody dostępu asPoint()umożliwiającej uzyskanie informacji konkretnie o danym punkcie. Pozwoli to uniknąć złamania wszystkich zasad, ale moim zdaniem utrudni zrozumienie kodu. Trzecią opcją jest użycie automatycznego generowania metody równości przy użyciu IDE. Nawiasem mówiąc, pomysł odtwarza generację Horstmanna, umożliwiając wybór strategii wdrożenia metody w nadklasie lub u jej potomków. Wreszcie następna reguła spójności stwierdza, że ​​nawet jeśli obiekty się xnie yzmienią, ponowne wywołanie ich x.equals(y)musi zwrócić tę samą wartość co poprzednio. Ostatnia zasada jest taka, że ​​żaden obiekt nie powinien być równy null. Tutaj wszystko jest jasne null- to jest niepewność, czy przedmiot jest równy niepewności? Nie jest to jasne, tj false.

Ogólny algorytm wyznaczania równych

  1. Sprawdź równość odwołań do obiektów thisi parametrów metod o.
    if (this == o) return true;
  2. Sprawdź, czy link jest zdefiniowany o, tzn. czy jest null.
    Jeżeli w przyszłości przy porównywaniu typów obiektów zostanie użyty operator instanceof, to element ten można pominąć, gdyż falsew tym przypadku parametr ten zwraca null instanceof Object.
  3. Porównaj typy obiektów thisza pomocą ooperatora instanceoflub metody getClass(), kierując się powyższym opisem i własną intuicją.
  4. Jeśli metoda equalszostanie przesłonięta w podklasie, pamiętaj o wykonaniu wywołaniasuper.equals(o)
  5. Konwertuj typ parametru ona wymaganą klasę.
  6. Wykonaj porównanie wszystkich znaczących pól obiektów:
    • dla typów pierwotnych (z wyjątkiem floati double), używając operatora==
    • w przypadku pól referencyjnych musisz wywołać ich metodęequals
    • w przypadku tablic można zastosować iterację cykliczną lub metodęArrays.equals()
    • dla typów floati doublekonieczne jest użycie metod porównawczych odpowiednich klas opakowań Float.compare()iDouble.compare()
  7. I na koniec odpowiedz na trzy pytania: czy zaimplementowana metoda jest symetryczna ? przechodni ? Zgadza się ? Pozostałe dwie zasady ( zwrotność i pewność ) są zwykle realizowane automatycznie.

Reguły zastępowania HashCode

Hash to liczba wygenerowana z obiektu, która opisuje jego stan w pewnym momencie. Liczba ta jest używana w Javie głównie w tabelach skrótów, takich jak HashMap. W takim przypadku funkcję mieszającą polegającą na uzyskaniu liczby na podstawie obiektu należy zaimplementować w taki sposób, aby zapewnić w miarę równomierny rozkład elementów w tablicy mieszającej. A także, aby zminimalizować prawdopodobieństwo kolizji, gdy funkcja zwraca tę samą wartość dla różnych kluczy.

Kod skrótu kontraktu

Aby zaimplementować funkcję skrótu, specyfikacja języka definiuje następujące zasady:
  • wywołanie metody hashCodewięcej niż raz na tym samym obiekcie musi zwrócić tę samą wartość skrótu, pod warunkiem, że pola obiektu biorące udział w obliczaniu wartości nie uległy zmianie.
  • wywołanie metody hashCodena dwóch obiektach powinno zawsze zwracać tę samą liczbę, jeśli obiekty są równe (wywołanie metody equalsna tych obiektach zwraca true).
  • wywołanie metody hashCodena dwóch nierównych obiektach musi zwrócić różne wartości skrótu. Chociaż ten wymóg nie jest obowiązkowy, należy wziąć pod uwagę, że jego wdrożenie będzie miało pozytywny wpływ na wydajność tablic skrótów.

Metody równości i hashCode muszą zostać zastąpione razem

Z umów opisanych powyżej wynika, że ​​nadpisując metodę w kodzie equals, należy zawsze zastąpić metodę hashCode. Ponieważ w rzeczywistości dwie instancje klasy są różne, ponieważ znajdują się w różnych obszarach pamięci, należy je porównać według pewnych logicznych kryteriów. W związku z tym dwa logicznie równoważne obiekty muszą zwracać tę samą wartość skrótu. Co się stanie, jeśli tylko jedna z tych metod zostanie zastąpiona?
  1. equalstak hashCodenie

    Załóżmy, że poprawnie zdefiniowaliśmy metodę equalsw naszej klasie i hashCodezdecydowaliśmy się pozostawić ją w niezmienionej postaci Object. Wtedy z punktu widzenia metody equalsoba obiekty będą logicznie równe, natomiast z punktu widzenia metody hashCodenie będą miały ze sobą nic wspólnego. Dlatego umieszczając obiekt w tablicy skrótów, ryzykujemy, że nie uda nam się go odzyskać za pomocą klucza.
    Na przykład tak:

    Map<Point, String> m = new HashMap<>();
    m.put(new Point(1, 1),Point A);
    // pointName == null
    String pointName = m.get(new Point(1, 1));

    Oczywiście obiekt umieszczany i obiekt poszukiwany to dwa różne obiekty, chociaż logicznie są sobie równe. Ale ponieważ mają różne wartości skrótu, ponieważ naruszyliśmy umowę, możemy powiedzieć, że zgubiliśmy nasz obiekt gdzieś w trzewiach tablicy mieszającej.

  2. hashCodetak equalsnie.

    Co się stanie, jeśli zastąpimy metodę hashCodei equalsodziedziczymy implementację metody z klasy Object? Jak wiadomo, equalsmetoda domyślna po prostu porównuje wskaźniki z obiektami, sprawdzając, czy odnoszą się one do tego samego obiektu. Załóżmy, że hashCodenapisaliśmy metodę według wszystkich kanonów, czyli wygenerowaliśmy ją za pomocą IDE i zwróci te same wartości skrótu dla logicznie identycznych obiektów. Oczywiście w ten sposób zdefiniowaliśmy już pewien mechanizm porównywania dwóch obiektów.

    Zatem teoretycznie należy przeprowadzić przykład z poprzedniego akapitu. Ale nadal nie będziemy w stanie znaleźć naszego obiektu w tablicy mieszającej. Chociaż będziemy blisko tego, bo przynajmniej znajdziemy koszyk z tablicą mieszającą, w którym będzie leżał obiekt.

    Aby skutecznie wyszukać obiekt w tablicy mieszającej, oprócz porównania wartości skrótu klucza, stosuje się również określenie logicznej równości klucza z poszukiwanym obiektem. Oznacza to, że equalsnie można obejść się bez zastąpienia metody.

Ogólny algorytm wyznaczania hashCode

Tutaj wydaje mi się, że nie powinieneś się zbytnio przejmować i wygenerować metodę w swoim ulubionym IDE. Bo te wszystkie przesunięcia bitów w prawo i w lewo w poszukiwaniu złotego podziału, czyli rozkładu normalnego - to już dla totalnie upartych kolesi. Osobiście wątpię, czy uda mi się zrobić to lepiej i szybciej niż ten sam Idea.

Zamiast wniosków

Widzimy zatem, że metody equalsodgrywają hashCodedobrze zdefiniowaną rolę w języku Java i mają na celu uzyskanie cechy równości logicznej dwóch obiektów. W przypadku metody equalsma to bezpośredni związek z porównywaniem obiektów, w przypadku hashCodepośrednim, gdy konieczne jest, powiedzmy, określenie przybliżonej lokalizacji obiektu w tablicach mieszających lub podobnych strukturach danych, aby zwiększyć prędkość wyszukiwania obiektu. Oprócz umów equalsistnieje hashCodejeszcze jeden wymóg związany z porównywaniem obiektów. Jest to spójność metody compareTointerfejsu Comparablez equals. Wymóg ten zobowiązuje programistę do zwracania zawsze, x.equals(y) == truegdy x.compareTo(y) == 0. Oznacza to, że widzimy, że logiczne porównanie dwóch obiektów nie powinno być sprzeczne w żadnym miejscu aplikacji i powinno być zawsze spójne.

Źródła

Efektywna Java, wydanie drugie. Jozuego Blocha. Darmowe tłumaczenie bardzo dobrej książki. Java, biblioteka profesjonalisty. Tom 1. Podstawy. Kay Horstmann. Trochę mniej teorii, a więcej praktyki. Ale nie wszystko jest analizowane tak szczegółowo, jak Bloch. Chociaż istnieje widok na ten sam równa się (). Struktury danych na zdjęciach. HashMap Niezwykle przydatny artykuł na temat urządzenia HashMap w Javie. Zamiast sięgać do źródeł.
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION