1. Porównanie record i class: jakie są najważniejsze różnice?
W Javie mamy dwa podstawowe sposoby opisywania własnych typów danych: poprzez zwykłe klasy (class) oraz klasy record (record). Na pierwszy rzut oka oba warianty pozwalają przechowywać i przetwarzać dane. Ale jeśli zajrzymy głębiej, różnic jest więcej, niż może się wydawać!
Tabela różnic: class vs record
| Charakterystyka | Zwykła klasa (class) | Klasa record (record) |
|---|---|---|
| Mutowalność | Dowolna: pola mogą być final lub nie | Niemutowalny: wszystkie pola są final |
| Dziedziczenie | Można dziedziczyć (extends), domyślnie nie jest final | Zawsze final, nie może być nadklasą |
| Pola | Dowolne: statyczne, niestatyczne, final lub nie-final, dowolne typy | Tylko komponenty record (private final), plus pola statyczne |
| Gettery/settery | Sami piszemy (albo generujemy za pomocą Lomboka) | Automatycznie tworzone są gettery (nazwa pola jako nazwa metody), setterów brak |
| equals/hashCode/toString | Zwykle piszemy ręcznie/generujemy (equals, hashCode, toString) | Generowane automatycznie dla wszystkich komponentów |
| Konstruktory | Dowolne, w dowolnej liczbie | Jeden główny (po wszystkich komponentach), można dodać konstruktor kompaktowy |
| Interfejsy | Można implementować | Można implementować |
| Dodatkowe metody | Dowolne | Można dodawać, ale tylko metody (nie pola) |
| Użycie w kolekcjach | Można, ale trzeba poprawnie zaimplementować equals/hashCode | Idealnie nadaje się na klucze/wartości, wszystko już jest zaimplementowane |
Przykład poglądowy
Zwykła klasa:
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
Klasa record:
public record Person(String name, int age) { }
I to wszystko! Jedna linijka kodu — i otrzymujemy to samo (a nawet lepiej). I żadnego ryzyka, że zapomnimy zaimplementować coś istotnego.
2. Ograniczenia klas record
Klasy record to nie tylko „krótka składnia”, lecz odrębna koncepcja z własnymi, rygorystycznymi zasadami. Przyjrzyjmy się im bliżej.
Record zawsze final
Klasa record z definicji jest zawsze final. To znaczy, że nie możesz utworzyć podklasy od record:
public record Point(int x, int y) { }
// public class ColoredPoint extends Point { } // Błąd kompilacji!
Jeśli musisz rozszerzać zachowanie, użyj zwykłych klas albo kompozycji (umieść record wewnątrz klasy).
Record nie może być nadklasą
Klasa record nie może być rodzicem dla innych klas, zawsze jest final. To logiczne: gdyby było to możliwe, ktoś mógłby dodać pole mutowalne — i cała koncepcja „niemutowalnych danych” by upadła.
Tylko pola final (komponenty)
Wszystkie komponenty record deklaruje się w nagłówku i są domyślnie private final. Nie możesz dodać niestatycznych pól w ciele record:
public record User(String login, String email) {
// int counter; // Błąd! Nie można dodawać pól niestatycznych
static int totalUsers = 0; // Można, to pole statyczne
}
Brak setterów
Klasa record nie może mieć setterów dla komponentów. Każda próba dodania metody w rodzaju setX(int x) będzie bezsensowna: nie da się zmienić wartości pola po utworzeniu obiektu.
public record Point(int x, int y) {
// public void setX(int x) { this.x = x; } // Błąd: nie można zmienić pola final
}
Brak pustego konstruktora
Klasa record ma zawsze tylko główny konstruktor przyjmujący wartości dla wszystkich komponentów. Nie można utworzyć record bez podania wszystkich danych:
Point p = new Point(1, 2); // OK
// Point p = new Point(); // Błąd: brak konstruktora bez parametrów
Brak niestatycznych inicjalizatorów
Klasa record nie może zawierać niestatycznych inicjalizatorów (tych, które zapisuje się w klamrach poza metodami):
public record User(String login) {
// { /* ... */ } // Błąd: niestatyczne inicjalizatory są zabronione
}
Ograniczenia dziedziczenia
Klasa record nie może jawnie dziedziczyć po innej klasie (poza java.lang.Record, która jest dla nas ukryta jako klasa bazowa wszystkich recordów). Ale implementacja interfejsów — jak najbardziej!
public interface Printable {
void print();
}
public record Book(String title) implements Printable {
@Override
public void print() {
System.out.println("Drukujemy książkę: " + title);
}
}
Nie nadaje się do złożonej logiki biznesowej
record dotyczy danych, a nie zachowania. Jeśli obiekt ma złożoną logikę, stan mutowalny, „cykl życia” lub mnóstwo zależności — record tu nie pomoże. Lepiej użyć zwykłej klasy.
3. Kiedy warto używać klas record?
- DTO (Data Transfer Object): do przekazywania niemutowalnych danych między warstwami aplikacji, serwisami, mikroserwisami czy kontrolerami REST (np. w odpowiedziach JSON).
- Value Object: obiekty, które są definiowane wyłącznie przez swoje wartości.
- Klucze i wartości w kolekcjach: gdy istotna jest poprawna implementacja equals i hashCode (np. do użycia w HashMap lub Set).
- Wyniki obliczeń: gdy trzeba zwrócić z metody kilka wartości naraz (np. record Pair<T, U>(T first, U second)).
Przykład: DTO dla kontrolera REST
public record UserDto(String login, String email) { }
Teraz można bezpiecznie zwracać obiekt tego typu z kontrolera bez obaw, że ktoś zmieni jego pola.
Przykład: klucz dla HashMap
public record Point(int x, int y) { }
Map<Point, String> pointNames = new HashMap<>();
pointNames.put(new Point(1, 2), "A");
pointNames.put(new Point(3, 4), "B");
// Wszystko działa poprawnie: equals i hashCode są już zaimplementowane!
4. Kiedy NIE należy używać klas record
- Stan mutowalny: jeśli choć jedno pole ma się zmieniać po utworzeniu obiektu.
- Złożona logika: jeśli obiekt ma złożone zachowanie, wiele metod, zagnieżdżone obiekty o stanie mutowalnym.
- Dziedziczenie: jeśli wymagana jest hierarchia klas, klasy abstrakcyjne, nadpisywanie metod.
- Encje logiki biznesowej: np. obiekty, które żyją w bazie danych i mają unikalny identyfikator.
Przykład: kiedy potrzebna jest zwykła klasa
public class Account {
private String id;
private int balance;
public Account(String id, int balance) {
this.id = id;
this.balance = balance;
}
public void deposit(int amount) { balance += amount; }
public void withdraw(int amount) { balance -= amount; }
// getters, setters, equals, hashCode, toString...
}
Tutaj wyraźnie widać, że stan obiektu się zmienia — record nie pasuje.
5. Praktyczne przykłady: wybór między record a class
Przykład 1: record — wybór idealny
public record Rectangle(int width, int height) {
public int area() {
return width * height;
}
}
- Prostokąt jest określony tylko przez szerokość i wysokość.
- Nie ma potrzeby zmiany tych wartości po utworzeniu.
- Można dodać użyteczną metodę area().
- Resztę Java zrobi za ciebie.
Przykład 2: class — najlepszy wariant
public class MutableRectangle {
private int width;
private int height;
public MutableRectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int area() { return width * height; }
}
Trzeba zmieniać wymiary prostokąta po utworzeniu? Użyj zwykłej klasy.
6. Typowe błędy przy pracy z klasami record
Błąd nr 1: próba dodania pola niestatycznego.
Klasa record nie pozwala deklarować niestatycznych pól poza listą komponentów. Jeśli spróbujesz — kompilator zgłosi błąd. Na przykład:
public record City(String name) {
// int population; // Błąd!
}
Błąd nr 2: chęć dodania settera.
Record nie obsługuje setterów dla komponentów. Każda próba zmiany wartości pola po utworzeniu obiektu kończy się błędem kompilacji.
Błąd nr 3: próba dziedziczenia record lub po record.
Record jest zawsze final. Nie można dziedziczyć po record, a record nie może dziedziczyć innej klasy (poza ukrytą java.lang.Record).
Błąd nr 4: używanie record dla obiektów mutowalnych.
Jeśli planujesz zmieniać stan obiektu po utworzeniu — record nie jest dla ciebie! Użyj zwykłej klasy.
Błąd nr 5: zapomniano o ograniczeniach konstruktora.
Klasa record musi mieć konstruktor przyjmujący wartości dla wszystkich komponentów. Nie ma konstruktora bez parametrów!
GO TO FULL VERSION