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?” |
|
Zawartość obiektów | Równość według logiki biznesowej |
|
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.
GO TO FULL VERSION