CodeGym /Kursy /JAVA 25 SELF /Kontrakty equals i hashCode

Kontrakty equals i hashCode

JAVA 25 SELF
Poziom 29 , Lekcja 0
Dostępny

1. Wprowadzenie

Jak porównujemy obiekty

W Javie obiekty to nie tylko dane — każdy ma własny adres w pamięci. Operator == odpowiada na pytanie „czy to to samo pudełko?”, czyli porównuje referencje (adresy), a nie zawartość. Metoda equals służy do porównywania właśnie zawartości. Domyślnie, jeśli jej nie nadpiszesz, equals zachowuje się jak ==.

Person p1 = new Person("Ivan", 20);
Person p2 = new Person("Ivan", 20);

System.out.println(p1 == p2); // false — to różne obiekty w pamięci!

Aby dwa różne obiekty były uznane za równe pod względem danych (np. gdy wszystkie istotne pola się pokrywają), należy nadpisać equals. A jeśli planujesz używać obiektów w kolekcjach haszujących, koniecznie wraz z nim poprawnie nadpisz także hashCode.

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // to ten sam obiekt
        if (o == null || getClass() != o.getClass()) return false; // sprawdzamy klasę
        Person person = (Person) o; // rzutujemy na właściwy typ
        return age == person.age && name.equals(person.name); // porównujemy pola
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age); // aby działało z HashSet/HashMap
    }
}

Takie nadpisanie jest krytyczne dla poprawnego działania z HashSet/HashMap: bez equals i hashCode kolekcje będą traktować nawet obiekty identyczne danymi jako różne.

Relacja równoważności w equals zależy od Twoich wymagań: można porównywać po wszystkich polach, po części pól (np. tylko email w User) — ważna jest spójność i przestrzeganie kontraktu.

Gdzie ma to szczególne znaczenie?

  • W kolekcjach opartych na tablicach haszujących: HashSet, HashMap, LinkedHashSet itp.
  • Przy wyszukiwaniu i usuwaniu elementów w kolekcjach: bez poprawnego equals poszukiwany obiekt może się „nie znaleźć”.
  • W logice biznesowej: na przykład dwóch User z tym samym email powinno być traktowanych jako jeden użytkownik.

hashCode — po co jest potrzebny?

Kolekcje haszujące (np. HashSet, HashMap) używają tablic haszujących. Metoda hashCode oblicza liczbę całkowitą — „adres koszyka”, do którego trafi obiekt. Jeśli dwa obiekty są równe według equals, ich hashCode musi być identyczny. Złamiesz tę zasadę — a kolekcje zaczną zachowywać się nieprzewidywalnie.

2. Kontrakt equals i hashCode

Kontrakt equals

Podstawowe wymagania dotyczące zachowania equals:

  • Zwrotność: a.equals(a) zawsze zwraca true.
  • Symetryczność: jeśli a.equals(b) — true, to i b.equals(a) — true.
  • Tranzytywność: jeśli a.equals(b) i b.equals(c), to a.equals(c) — również true.
  • Spójność: przy niezmienności obiektów wynik wywołań jest stabilny.
  • Porównanie z null: dowolny obiekt nie jest równy null.

Kontrakt hashCode

  • Jeśli dwa obiekty są równe według equals, ich hashCode są równe.
  • Jeśli obiekty nie są równe, ich kody haszujące mogą się pokrywać (kolizje są dopuszczalne, ale niepożądane).
  • Dopóki obiekt nie zmienia się logicznie, jego hashCode powinien pozostawać stały.

Innymi słowy, identyczny hashCode to warunek konieczny, lecz niewystarczający równości: taki sam hash nie gwarantuje równości według equals.

3. Implementacja equals i hashCode: przykład

Rozważmy klasę Person, w której równość jest określana przez pola name i age.

public class Person {
    private String name;
    private int age;

    // Konstruktor, gettery, settery...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // Porównanie referencji
        if (o == null || getClass() != o.getClass()) return false; // Sprawdzenie klasy

        Person person = (Person) o; // Rzutowanie typu

        // Porównujemy pola
        return age == person.age &&
               (name != null ? name.equals(person.name) : person.name == null);
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age; // 31 — popularny wybór liczby pierwszej
        return result;
    }
}
  • Najpierw szybkie sprawdzenia: referencja i klasa.
  • Następnie porównanie istotnych pól.
  • W hashCode użyto liczby pierwszej 31, aby zmniejszyć liczbę kolizji.

Użycie Objects.equals i Objects.hash

Począwszy od Java 7, klasa Objects upraszcza kod i czyni go bezpieczniejszym względem null:

import java.util.Objects;

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    return age == person.age &&
           Objects.equals(name, person.name);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

4. equals, hashCode i compareTo: jak są powiązane

Jak powiązane są equals i compareTo?

Interfejs Comparable definiuje metodę compareTo, która zwraca liczbę ujemną/zero/dodatnią dla „mniej/równo/więcej”. Zalecane jest, aby z a.compareTo(b) == 0 wynikało a.equals(b). W drugą stronę nie jest to wymagane.

Gdy naruszymy spójność (np. compareTo porównuje tylko po wieku, a equals — po imieniu i wieku), kolekcje sortowane, takie jak TreeSet/TreeMap, mogą zachowywać się zaskakująco: obiekty są „równe” z punktu widzenia porządku, ale nie równe co do zawartości.

equals i hashCode w kolekcjach

  • W HashSet i HashMap operacje dodawania/wyszukiwania/usuwania opierają się na poprawnej implementacji equals i hashCode.
  • Bez nadpisania tych metod kolekcje uznają obiekty za „różne”, nawet jeśli ich dane są identyczne.

5. Przykłady: jak to działa w kolekcjach

HashSet: przechowywanie unikalnych obiektów

Set<Person> people = new HashSet<>();
people.add(new Person("Ivan", 20));
people.add(new Person("Ivan", 20)); // Duplikat

System.out.println(people.size()); // 1, jeśli equals/hashCode są poprawnie zaimplementowane

Bez poprawnego equals/hashCode do zbioru trafią oba obiekty.

HashMap: wyszukiwanie po kluczu

Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Anna", 25);
Person p2 = new Person("Anna", 25);

map.put(p1, "Użytkownik 1");
System.out.println(map.get(p2)); // "Użytkownik 1", jeśli equals/hashCode są poprawnie zaimplementowane

Bez kontraktu kolekcja zwróci null — dla niej to „różne” klucze.

6. Best practices: wskazówki dotyczące implementacji

  • Uwzględniaj w equals/hashCode wszystkie pola definiujące „tożsamość” obiektu.
  • Nie używaj pól zmiennych (które mogą zmienić się po dodaniu do kolekcji) przy obliczaniu hashCode.
  • Zlecaj generowanie metod IDE — mniejsze ryzyko literówek.
  • W equals najpierw sprawdzaj this == o, potem klasę, a następnie pola.
  • Do porównywania pól-obiektów używaj Objects.equals.
  • Do kodu haszującego używaj Objects.hash lub sprawdzonego wzorca z mnożnikiem 31.

7. Przydatne szczegóły

Dlaczego nie można używać tylko hashCode?

Kolizje są nieuniknione: różne obiekty mogą mieć taki sam hashCode. Hash jest jedynie szybkim wskaźnikiem koszyka; ostateczną decyzję o równości podejmuje equals.

Czy można nie nadpisywać equals i hashCode?

Tylko jeśli masz pewność, że obiekty nigdy nie będą porównywane po zawartości i nie staną się kluczami/unikalnymi elementami w kolekcjach. W praktyce to rzadkość.

Różnica między ==, equals i compareTo

Operator/metoda Co porównuje? Do czego służy?
==
Referencje (adresy w pamięci) Sprawdzenie „czy to ten sam obiekt?”
equals
Zawartość obiektów Równość według logiki biznesowej
compareTo
Porządek (mniej/równo/więcej) Sortowanie, porządkowanie

8. Typowe błędy przy implementacji equals i hashCode

Błąd nr 1: Nadpisano equals, ale zapomniano o hashCode. Obiekty są uznawane za równe, lecz trafiają do różnych koszyków tablicy haszującej — wyszukiwanie i usuwanie się psuje.

Błąd nr 2: Używasz zmiennych pól w hashCode. Jeśli pole zmieni się po dodaniu do kolekcji, obiekt „zginie”: hash się zmieni, a koszyk — nie.

Błąd nr 3: Naruszona symetryczność/tranzytywność equals. a.equals(b) daje true, a b.equals(a) — false, albo łamana jest tranzytywność — kolekcje zaczynają zachowywać się nieprzewidywalnie.

Błąd nr 4: Brak sprawdzania klasy w equals. Porównywanie obiektów różnych klas prowadzi do błędnych wyników lub wyjątków.

Błąd nr 5: Porównujesz napisy i obiekty przy użyciu ==. Operator == porównuje referencje; używaj equals do zawartości.

Błąd nr 6: Brak spójności między compareTo i equals. Jeśli a.compareTo(b) == 0, ale !a.equals(b), kolekcje TreeSet/TreeMap mogą uznać elementy za równe dla porządku, lecz różne dla równości — to źródło „zjaw” i duplikatów.

1
Zadanie
JAVA 25 SELF, poziom 29, lekcja 0
Niedostępne
Ewidencja unikalnych miast w wirtualnym świecie 🗺️
Ewidencja unikalnych miast w wirtualnym świecie 🗺️
1
Zadanie
JAVA 25 SELF, poziom 29, lekcja 0
Niedostępne
Zarządzanie pracownikami w nowym systemie HR 👥
Zarządzanie pracownikami w nowym systemie HR 👥
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION