1. Automatyczne generowanie equals, hashCode, toString
Po co są te metody?
Pracując z obiektami w Javie, dość szybko trafiasz na te same zadania. Czasem trzeba sprawdzić, czy dwa obiekty są równe. Na przykład ustalić, czy obiekt już jest w kolekcji takiej jak Set lub Map. W innych przypadkach obiekt jest używany jako klucz w HashMap i wtedy bez specjalnych zasad porównywania ani rusz. A jeszcze niemal zawsze chcesz wydrukować obiekt w logu lub na ekranie tak, aby wynik nie był bełkotem w rodzaju MyClass@7b23ec81, tylko czymś sensownym.
Właśnie na takie sytuacje każda klasa w Javie ma trzy szczególne metody:
- equals(Object o) odpowiada za sprawdzanie równości.
- hashCode() daje obiektowi numeryczny „odcisk”, potrzebny kolekcjom pokroju tablic haszujących.
- toString() zwraca wygodną tekstową reprezentację obiektu, co bardzo ułatwia debugowanie i drukowanie.
Dlaczego to bywa bolesne w zwykłych klasach?
W zwykłych klasach te metody trzeba pisać ręcznie. I tu zaczyna się nuda i ból głowy. Powstaje masa szablonowego kodu, który tylko zaśmieca klasę. Bardzo łatwo się gdzieś pomylić: zapomnieć porównać pole, źle policzyć hashCode, a potem łapać zagadkowe bugi. A jeśli do klasy dodasz nowe pole — trzeba będzie znów zajrzeć do tych metod i wszystko poprawić.
Przykład zwykłej klasy
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; }
public int y() { return y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return 31 * x + y;
}
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
Brzmi znajomo? Tak, i to tylko dla dwóch pól! A co, jeśli jest ich dwadzieścia?
Jak robi to record
Klasa record robi to wszystko za ciebie. Wystarczy zadeklarować:
public record Point(int x, int y) { }
I Java sama wygeneruje:
- Konstruktor
- Gettery (x(), y())
- equals, hashCode, toString
Automatycznie wygenerowane metody
- equals porównuje wszystkie komponenty rekordu po wartości.
- hashCode jest obliczany na podstawie wszystkich komponentów.
- toString zwraca ciąg w postaci Point[x=1, y=2].
Zobaczmy to w praktyce!
public record Point(int x, int y) {}
public class Demo {
public static void main(String[] args) {
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // true
System.out.println(p1); // Point[x=1, y=2]
}
}
Wynik:
true
true
Point[x=1, y=2]
Wszystko działa zgodnie z oczekiwaniami — bez ani jednej zbędnej linijki kodu!
2. Dlaczego to jest ważne: kolekcje, debugowanie i bezpieczeństwo
Poprawne działanie w kolekcjach
Wyobraź sobie, że używasz obiektów jako kluczy w HashMap lub elementów w HashSet. Jeśli equals i hashCode są zaimplementowane błędnie — kolekcje będą zachowywać się dziwnie: nie znajdą elementu, który właśnie dodałeś, albo przeciwnie — uznają dwa różne obiekty za identyczne.
Z klasami record możesz mieć pewność: porównanie i hash zawsze uwzględniają wszystkie komponenty rekordu (w kolejności, w której zostały zadeklarowane).
Przykład: użycie rekordu jako klucza
import java.util.HashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
record Point(int x, int y) {}
Map<Point, String> map = new HashMap<>();
Point p1 = new Point(3, 4);
map.put(p1, "Hello!");
Point p2 = new Point(3, 4);
System.out.println(map.get(p2)); // "Hello!" — działa!
}
}
Zwróć uwagę: p1 i p2 to różne obiekty (różne referencje), ale zawierają takie same wartości pól, więc są uznawane za równe. A więcej o Map i HashMap poznasz na poziomie 26 :P
Wygoda debugowania i logowania
Zamiast smutnego Point@1a2b3c4d (jak bywa domyślnie w zwykłych klasach) rekord wypisuje się ładnie i informacyjnie:
Point[x=3, y=4]
To świetnie oszczędza czas podczas debugowania i logowania.
3. Jak działają equals, hashCode, toString wewnątrz rekordu
Metoda equals
Rekord implementuje equals tak, że dwa obiekty są uznawane za równe, jeśli:
- Są tego samego typu (tej samej klasy rekordu)
- Wszystkie ich komponenty są równe (== dla prymitywów, equals() dla obiektów)
Przykład porównania
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point p3 = new Point(1, 3);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.equals(p3)); // false
Metoda hashCode
Kod skrótu jest obliczany na podstawie wszystkich komponentów rekordu, zwykle za pomocą standardowej metody Objects.hash(...).
System.out.println(p1.hashCode()); // Na przykład 994
System.out.println(p2.hashCode()); // Też 994
System.out.println(p3.hashCode()); // Inna liczba
Metoda toString
Reprezentacja tekstowa ma zawsze format:
ClassName[field1=value1, field2=value2, ...]
System.out.println(p1); // Point[x=1, y=2]
4. Nadpisywanie equals, hashCode, toString: kiedy i jak?
Czasem (rzadko, ale bywa) trzeba zmienić standardowe zachowanie tych metod. Na przykład chcesz, aby toString zwracał ciąg w innym formacie, albo żeby porównanie odbywało się tylko po części pól.
Uwaga: jeśli nadpisujesz equals/hashCode, rób to bardzo świadomie! Naruszenie ich „kontraktu” może prowadzić do błędów, które trudno wychwycić.
Jak nadpisać metodę
Po prostu zdefiniuj własną metodę wewnątrz ciała rekordu:
public record Point(int x, int y) {
@Override
public String toString() {
return "(" + x + "; " + y + ")";
}
}
Point p = new Point(3, 5);
System.out.println(p); // (3; 5)
Czy można nadpisać equals/hashCode?
Tak, ale jest to zdecydowanie odradzane, jeśli nie masz pewności, co robisz. Na przykład gdy chcesz porównywać tylko po polu x (co już jest dziwne):
public record Point(int x, int y) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point other)) return false;
return x == other.x;
}
@Override
public int hashCode() {
return Integer.hashCode(x);
}
}
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 999);
System.out.println(p1.equals(p2)); // true (!)
Ale bądź ostrożny: jeśli nadpisujesz equals, zawsze nadpisz też hashCode — w przeciwnym razie kolekcje będą działać niepoprawnie.
Dobre praktyki
- Jeśli nie wiesz dokładnie, po co nadpisywać — nie nadpisuj!
- Dla toString — możesz śmiało zrobić własny format, jeśli chcesz.
- Dla equals/hashCode — tylko jeśli masz ważny powód i rozumiesz konsekwencje.
5. Praktyka: porównywanie obiektów i użycie rekordów w kolekcjach
Przykład: porównanie dwóch obiektów record
public record User(String name, int age) {}
public class Demo {
public static void main(String[] args) {
User u1 = new User("Alice", 20);
User u2 = new User("Alice", 20);
User u3 = new User("Bob", 25);
System.out.println(u1.equals(u2)); // true
System.out.println(u1.equals(u3)); // false
System.out.println(u1.hashCode() == u2.hashCode()); // true
System.out.println(u1); // User[name=Alice, age=20]
}
}
Przykład: użycie rekordu jako klucza w HashMap
Wyobraźmy sobie, że mamy aplikację, w której przechowujemy liczbę odwiedzin użytkowników według ich imienia i wieku (kto wie — może w klubie są dwaj „Ivan, 20 lat”).
import java.util.HashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
record User(String name, int age) {}
Map<User, Integer> visits = new HashMap<>();
User ivan20 = new User("Ivan", 20);
User ivan22 = new User("Ivan", 22);
visits.put(ivan20, 5);
visits.put(ivan22, 2);
// Sprawdźmy, że wyszukiwanie po wartości działa poprawnie
System.out.println(visits.get(new User("Ivan", 20))); // 5
System.out.println(visits.get(new User("Ivan", 22))); // 2
}
}
Gdyby equals i hashCode nie były zaimplementowane poprawnie, wyszukiwanie by nie zadziałało. A więcej o Map i HashMap dowiesz się z wykładów poziomu 26 :P
6. Typowe błędy przy pracy z equals, hashCode, toString w klasach record
Błąd nr 1: Oczekiwanie, że pola można zmieniać po utworzeniu.
Pola rekordu są zawsze final, a porównanie odbywa się na podstawie ich wartości nadanych w konstruktorze. Jeśli jakimś podstępnym sposobem „zmieniasz” stan wewnętrzny (np. przez modyfikowalny obiekt wewnątrz pola), porównanie i hash mogą stać się niepoprawne.
Błąd nr 2: Nadpisano equals, ale zapomniano o hashCode.
Jeśli nadpisujesz jedną z tych metod — zawsze nadpisuj też drugą! W przeciwnym razie kolekcje (HashSet, HashMap) będą zachowywać się nieprzewidywalnie.
Błąd nr 3: Oczekiwanie, że toString będzie w innym formacie.
Jeśli potrzebujesz szczególnego formatu ciągu — po prostu nadpisz toString. Domyślnie format to zawsze ClassName[field1=value1, field2=value2].
Błąd nr 4: Używanie rekordu dla złożonych klas z polami modyfikowalnymi.
Pola rekordu powinny być niezmienialne. Jeśli jako pole wykorzystujesz na przykład ArrayList i ktoś zmienia jego zawartość — porównanie i kod skrótu mogą się „rozsypać”. Do rekordów najlepiej używać tylko typów niezmienialnych.
Błąd nr 5: Używanie rekordu dla klas, które nie są obiektami wartości (value object).
Record to nie „mała klasa z krótką składnią”. To właśnie value object, przeznaczony do przechowywania zestawu wartości. Jeśli masz złożoną logikę, stan modyfikowalny lub potrzebę dziedziczenia — użyj zwykłej klasy.
GO TO FULL VERSION