Java Academy · Lekcja

Kontrakt equals/hashCode

Dlaczego obie metody muszą być zgodne

Lekcja 2 z 413 kroki

Kontrakt equals/hashCode to bezpłatna lekcja Java Academy na CoddyKit. To lekcja 2 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Java Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Java Academy zawiera 4 lekcji w sumie.

Dwie metody, jeden kontrakt

Każdy obiekt Java dziedziczy equals() i hashCode() po Object.

Przesłaniając jedną z nich, niemal zawsze trzeba przesłonić także drugą. Tworzą one wiążący kontrakt, na którym polegają kolekcje oparte na haszowaniu.

public class Main {
    public static void main(String[] args) {
        Object a = new Object();
        Object b = new Object();
        System.out.println(a.equals(b));
        System.out.println(a.hashCode() == b.hashCode());
    }
}

Podstawowa reguła

Kontrakt stanowi, że:

  • Jeśli a.equals(b) ma wartość true, to a.hashCode() musi być równe b.hashCode().
  • Jeśli kody skrótu się różnią, obiekty na pewno nie są równe.

Równe obiekty muszą mieć równe kody skrótu. Odwrotna zależność nie jest wymagana.

public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hel" + "lo";
        System.out.println(s1.equals(s2));
        System.out.println(s1.hashCode() == s2.hashCode());
    }
}

Nierówne obiekty mogą mieć ten sam kod skrótu

Dwa nierówne obiekty mogą mieć ten sam kod skrótu. To po prostu kolizja, którą mapa obsługuje za pomocą equals().

Jedyną niedozwoloną sytuacją są więc równe obiekty o różnych kodach skrótu.

public class Main {
    public static void main(String[] args) {
        // Equal value, unequal objects, same hash is fine
        System.out.println("FB".hashCode() == "Ea".hashCode());
        System.out.println("FB".equals("Ea"));
    }
}

Klasa, która przesłania tylko equals

Oto błędna klasa. Przesłania equals(), ale nie hashCode().

Dwa logicznie równe punkty mają teraz różne kody skrótu odziedziczone po Object, co narusza kontrakt.

public class Main {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;
            return x == p.x && y == p.y;
        }
        // BUG: no hashCode override
    }
    public static void main(String[] args) {
        Point a = new Point(1, 2);
        Point b = new Point(1, 2);
        System.out.println("equals: " + a.equals(b));
        System.out.println("same hash: " + (a.hashCode() == b.hashCode()));
    }
}

Błąd w HashSet

Błędna klasa nie działa poprawnie w HashSet. Zbiór szuka w kubełku wybranym na podstawie błędnego kodu skrótu i nie znajduje równego elementu.

W rezultacie powstają duplikaty, które powinny zostać odrzucone.

import java.util.HashSet;
import java.util.Set;

public class Main {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;
            return x == p.x && y == p.y;
        }
    }
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

Naprawa

Należy przesłonić obie metody i wyprowadzić ich implementacje z tych samych pól. Wtedy równe obiekty mają ten sam kod skrótu i trafiają do tego samego kubełka.

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class Main {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;
            return x == p.x && y == p.y;
        }
        @Override public int hashCode() { return Objects.hash(x, y); }
    }
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

equals musi być zwrotne

Poprawne equals() spełnia cztery właściwości. Pierwszą jest zwrotność: x.equals(x) musi zwracać true.

Brzmi to oczywiście, ale własna logika może złamać tę zasadę, jeśli nie uwzględni się przypadku tej samej referencji.

public class Main {
    public static void main(String[] args) {
        String x = "java";
        System.out.println(x.equals(x));
    }
}

Symetryczność i przechodniość

Dwie kolejne reguły:

  • Symetryczność: jeśli a.equals(b), to b.equals(a).
  • Przechodniość: jeśli a.equals(b) i b.equals(c), to a.equals(c).

Mieszanie typów w hierarchii klas często łamie symetryczność, dlatego klasy należy porównywać uważnie.

public class Main {
    public static void main(String[] args) {
        String a = "x", b = "x", c = "x";
        boolean symmetric = a.equals(b) == b.equals(a);
        boolean transitive = a.equals(b) && b.equals(c) && a.equals(c);
        System.out.println("symmetric: " + symmetric);
        System.out.println("transitive: " + transitive);
    }
}

Spójność

Czwarta reguła mówi o spójności: kolejne wywołania zwracają ten sam wynik, dopóki obiekty się nie zmieniają.

Dlatego equals i hashCode powinny opierać się na niezmiennych polach. Zmiana pola używanego przez hashCode po wstawieniu obiektu uszkadza mapę.

import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        // Immutable record gives consistent equals/hashCode automatically
        record Id(int value) {}
        Id id = new Id(7);
        System.out.println(Objects.equals(id, new Id(7)));
        System.out.println(id.hashCode() == new Id(7).hashCode());
    }
}

Rekordy robią to automatycznie

Java record automatycznie generuje equals() i hashCode() na podstawie wszystkich swoich komponentów.

W przypadku danych o charakterze wartościowym należy preferować rekordy. Kontrakt jest wtedy spełniony bez dodatkowej pracy i pozostaje spójny.

import java.util.HashSet;
import java.util.Set;

public class Main {
    record Point(int x, int y) {}
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

Pułapka mutowalnego klucza

Jeśli po umieszczeniu obiektu w mapie zmienią Państwo pole używane przez hashCode, nie będzie można go już odnaleźć.

Mapa szuka w starym kubełku, a obiekt ma teraz kod skrótu wskazujący na nowy kubełek. Wpis staje się utraconym widmem.

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class Main {
    static class Box {
        int id;
        Box(int id) { this.id = id; }
        @Override public boolean equals(Object o) {
            return o instanceof Box b && b.id == id;
        }
        @Override public int hashCode() { return Objects.hash(id); }
    }
    public static void main(String[] args) {
        Map<Box, String> m = new HashMap<>();
        Box key = new Box(1);
        m.put(key, "value");
        key.id = 99; // mutated after insertion
        System.out.println(m.get(key));
    }
}

Szybkie sprawdzenie

Proszę sprawdzić swoją znajomość kontraktu.

Podsumowanie

Poznali Państwo kontrakt equals/hashCode:

  • Równe obiekty muszą mieć równe kody skrótu.
  • equals musi być zwrotne, symetryczne, przechodnie i spójne.
  • Obie metody należy oprzeć na tych samych niezmiennych polach.
  • Rekordy automatycznie generują poprawne implementacje.

Następnie napiszą Państwo własny poprawny hashCode ręcznie.

import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        System.out.println("Contract recap: " + Objects.hash(1, 2, 3));
    }
}
Bezpłatny start

Ucz się Java dzięki korepetycjom AI — za darmo

Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.

Kursy
104
Lekcje
374

Często zadawane pytania

Czy lekcja „Kontrakt equals/hashCode” jest bezpłatna?

Tak — pełny tekst „Kontrakt equals/hashCode” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Java Academy, przejdź na CoddyKit PRO. Kurs Java Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Kontrakt equals/hashCode”?

Dlaczego obie metody muszą być zgodne Ćwiczysz Java Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Java Academy?

Nie wymagamy żadnego doświadczenia. Java Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 2 z 4.

Ile czasu zajmuje lekcja „Kontrakt equals/hashCode”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Java Academy?

Tak. Każda lekcja Java Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Jak działa HashMap
  2. Kontrakt equals/hashCode
  3. Implementowanie hashCode
  4. Konwersja do drzewa i wydajność
← Powrót do Java Academy