1. Klasy record i niezmienność
Niezmienność – to cecha obiektu, przy której jego stan nie może zostać zmieniony po utworzeniu. Innymi słowy: jeśli obiekt został utworzony, nie możesz zmienić jego wewnętrznych danych. Koniec! Jak wyryte w kamieniu.
Wyobraź sobie bilet na pociąg. Gdy tylko zostanie wydrukowany, nie możesz zmienić daty ani miejsca odjazdu (no, chyba że nie liczyć sprytnych kombinacji w Photoshopie). Bilet to obiekt niezmienny. Jeśli chcesz inny bilet – kupujesz nowy.
W programowaniu takie obiekty nazywa się immutable objects. Chronią program przed przypadkowymi zmianami, czynią kod bezpieczniejszym i bardziej przewidywalnym.
Cechy obiektu niezmiennego
- Wszystkie pola obiektu – final (można je przypisać tylko raz, zwykle w konstruktorze).
- Brak setterów (metod, które zmieniają wartości pól).
- Wszystkie metody zwracające dane wewnętrzne zwracają kopie albo same dane są również niezmienne.
Klasy record w Java zostały stworzone właśnie po to, aby prosto i bez bólu tworzyć niezmienne struktury danych.
Dlaczego record jest niezmienny?
- Wszystkie komponenty recordu – final.
Kiedy deklarujesz record, kompilator automatycznie nadaje wszystkim jego polom modyfikatory private final. Oznacza to, że po utworzeniu obiektu nie będziesz mógł zmienić jego pól. - Brak setterów.
W klasie record nie można dodać metody setX(int x) – kompilator nie pozwoli zmienić wartości pola po utworzeniu obiektu. - Konstruktor przypisuje wartości tylko raz.
Wszystkie wartości ustala się wyłącznie w momencie tworzenia obiektu.
Przykład
public record Point(int x, int y) {}
Point p = new Point(5, 10);
// p.x = 7; // Błąd kompilacji: pole x ma dostęp private i jest final
// p.x(7); // Błąd: brak settera!
System.out.println(p.x()); // 5
Próba zmiany pola lub wywołania nieistniejącego settera prowadzi do błędu kompilacji. Java rygorystycznie pilnuje kontraktu niezmienności.
2. Zalety obiektów niezmiennych
Bezpieczeństwo w środowisku wielowątkowym
W programach wielowątkowych (a takich jest dziś większość!) obiekty niezmienne to jak kamizelka kuloodporna. Skoro obiektu nie można zmienić, różne wątki mogą go swobodnie czytać, nie obawiając się, że ktoś w tym czasie podmieni dane. Nie trzeba synchronizować dostępu ani martwić się wyścigami danych.
Fakt: wiele klas w standardowej bibliotece Java, które intensywnie wykorzystuje się w scenariuszach wielowątkowych, jest niezmiennych lub specjalnie zabezpieczonych przed zmianami.
Uproszczenie zrozumienia kodu
Jeśli obiekt jest niezmienny, masz pewność: przekażesz go do innej metody lub klasy – i nie zmieni się „za twoimi plecami”. To bardzo ułatwia czytanie i debugowanie kodu. Nie trzeba zgadywać, kto i gdzie mógł zmienić pole – nikt nie mógł!
Wygodne jako klucze w kolekcjach
Obiekty niezmienne świetnie nadają się do użycia jako klucze w kolekcjach takich jak HashMap czy HashSet. Dlaczego? Bo ich equals i hashCode zależą wyłącznie od pól, które się nie zmieniają. Dzięki temu obiekt nie „zgubi się” w kolekcji z powodu zmiany stanu.
Mniej ukrytych błędów
Obiekt zmienny łatwo zepsuć, przypadkowo przekazując referencję nie tam, gdzie trzeba. Obiekt niezmienny jest jak drukowana książka: nikt nie może wyrwać ani przepisać strony.
3. Porównanie ze zwykłymi klasami
Porównajmy zachowanie zwykłej klasy i klasy record. Dla przykładu weźmiemy prosty model punktu na płaszczyźnie.
Zwykła klasa (mutable)
public class PointClass {
private int x;
private int y;
public PointClass(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
}
Można utworzyć obiekt, a potem dowolnie często zmieniać jego stan:
PointClass p = new PointClass(1, 2);
p.setX(10); // p.x ma teraz wartość 10
Klasa record (immutable)
public record Point(int x, int y) {}
Tworzysz obiekt – i to wszystko, pozostaje na zawsze taki, jaki został utworzony:
Point p = new Point(1, 2);
// p.x = 10; // Błąd! Brak dostępu do pola
// p.x(10); // Błąd! Brak settera
Tabela: porównanie zachowania
| Zwykła klasa | Klasa record | |
|---|---|---|
| Pola | dowolne | tylko private final |
| Settery | można dodać | nie można dodać |
| Zmienność | zmienny (mutable) | niezmienny |
| Autogeneracja | nie | tak (equals, hashCode, toString) |
4. Praktyka: jak wykorzystać niezmienność klas record
Zastosujmy klasę record w małym przykładzie. Załóżmy, że mamy aplikację bankową i chcemy przechowywać informacje o transakcji:
public record Transaction(String fromAccount, String toAccount, double amount) {}
Tworzymy obiekt:
Transaction t = new Transaction("12345", "67890", 1500.0);
System.out.println(t);
// Transaction[fromAccount=12345, toAccount=67890, amount=1500.0]
Spróbujmy „przelać” pieniądze na inne konto:
// t.toAccount = "11111"; // Błąd! Pole final, brak dostępu
// t.toAccount("11111"); // Błąd! Brak settera
Jeśli potrzebujemy innej transakcji – tworzymy nowy obiekt:
Transaction t2 = new Transaction(t.fromAccount(), "11111", t.amount());
Ważne: niezmienność nie znaczy „niewygodnie”. To po prostu inny styl pracy: jeśli potrzebujesz nowego stanu – tworzysz nowy obiekt.
5. Specyfika niezmienności: o czym trzeba pamiętać
Niezmienność – nie zawsze jest absolutna!
Klasa record gwarantuje, że jej pola się nie zmienią. Ale jeśli pole to referencja do obiektu zmiennego (na przykład tablica lub zwykła klasa), to zawartość tego obiektu może zostać zmieniona.
Przykład z tablicą
public record DataHolder(int[] data) {}
int[] arr = {1, 2, 3};
DataHolder holder = new DataHolder(arr);
arr[0] = 99;
System.out.println(holder.data()[0]); // 99! Tablica została zmieniona
Wniosek: jeśli chcesz prawdziwej niezmienności, używaj tylko typów niezmiennych (String, Integer, inne recordy itp.) albo twórz kopie obiektów zmiennych wewnątrz kanonicznego konstruktora klasy record. Na przykład:
int[] copy = Arrays.copyOf(data, data.length);
6. Jak samodzielnie uczynić zwykłą klasę niezmienną
Jeśli chcesz zrobić zwykłą klasę immutable (niezmienną), trzeba ręcznie:
- Oznaczyć wszystkie pola jako private final,
- Nie dodawać setterów,
- Inicjalizować wszystkie pola tylko przez konstruktor,
- Jeśli pole to obiekt zmienny, wykonywać kopię obronną (defensive copy).
Przykład
public final class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String name() {
return name;
}
public int age() {
return age;
}
}
Zgódź się, z klasą record jest to prostsze i krótsze:
public record User(String name, int age) {}
7. Typowe błędy przy pracy z niezmiennymi klasami record
Błąd nr 1: próba zmiany pola po utworzeniu.
Początkujący często próbują napisać p.x = 42; albo p.x(42); dla obiektu record. Ale kompilator od razu powie: „Tak nie można! Pole final, brak settera”.
Błąd nr 2: użycie obiektów zmiennych jako komponentów recordu.
Jeśli dodałeś do record pole typu List, Map, tablicę lub inny obiekt zmienny, sam record nie ochroni cię przed zmianami zawartości tego obiektu. Na przykład, jeśli masz record User(List<String> hobbies), ktoś może dodać lub usunąć element z listy i to zmieni stan twojego obiektu record. Aby tego uniknąć, używaj kolekcji niezmiennych (List.copyOf, Collections.unmodifiableList) lub wykonuj kopie kolekcji wewnątrz konstruktora recordu.
Błąd nr 3: błędne rozumienie niezmienności.
Niektórzy sądzą, że jeśli obiekt to record, jest chroniony przed wszelkimi zmianami. W rzeczywistości, jeśli pola są referencjami do obiektów zmiennych, ich zawartość można zmienić, co doprowadzi do nieoczekiwanych błędów.
GO TO FULL VERSION