CodeGym /Kursy /JAVA 25 SELF /Record: składnia, zalety

Record: składnia, zalety

JAVA 25 SELF
Poziom 22, Lekcja 0
Dostępny

1. Problem „klas DTO”: po co nam rekordy?

Bądźmy szczerzy: ile razy pisaliście taką klasę?


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 getX() {
        return x;
    }

    public int getY() {
        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 Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point{" + "x=" + x + ", y=" + y + '}';
    }
}

Niby nic trudnego, ale policzcie linie! A teraz wyobraźcie sobie, że macie 10 takich klas, a w każdej po 5–6 pól. Nawet IDE męczy się generowaniem tego szablonowego kodu. A jeśli postanowicie dodać nowe pole — trzeba będzie poprawić konstruktor, getter, equals, hashCode, toString... Nuda, rutyna i źródło błędów.

Takie klasy nazywa się DTO (Data Transfer Object) lub Value Object. One po prostu przechowują dane — i tyle. Jednak przez szablonowy kod trudno je utrzymywać.

Jeśli wydaje wam się, że to nie problem, poczekajcie, aż będziecie musieli zmienić 50 takich klas naraz. Wtedy z wyjątkowym ciepłem przypomnicie sobie o rekordach!

2. Wprowadzenie do record: składnia i magia Java 16+

Od Javy 16 wszystko się zmieniło. Pojawił się nowy rodzaj klas — record. Zostały stworzone specjalnie na przypadki, gdy trzeba po prostu przechowywać zestaw danych. Składnia jest niemal jak krotka w innych językach.

Jak zadeklarować record?


public record Point(int x, int y) { }

I... to wszystko! Właśnie utworzyliście niezmienną klasę z dwoma polami, konstruktorem, getterami, equals, hashCode i toString. Bez zbędnego klepania.

Co Java robi „pod maską”?

  • Pola x i y stają się private final.
  • Gettery tworzą się automatycznie: int x() i int y().
  • Konstruktor: public Point(int x, int y).
  • equals/hashCode: porównują wszystkie pola po wartości.
  • toString: zwraca łańcuch w postaci "Point[x=1, y=2]".

Można powiedzieć, że record to „DTO na sterydach”: mniej kodu, więcej gwarancji, mniej błędów.

Niezmienność (immutability)

Wszystkie pola rekordu są automatycznie final. Po utworzeniu obiektu nie można go zmienić — gwarantuje to kompilator.

Jeśli spróbujecie dodać setter lub zrobić pole nie-final — kompilator was zatrzyma. Taka troska o wasz spokój rzadko się zdarza!

3. Przykład użycia klasy record

Zwykła klasa (dużo kodu):


public class Client {
    private final String name;
    private final int id;

    public Client(String name, int id) {
        this.name = name;
        this.id = id;
    }

    public String getName() { return name; }
    public int getId() { return id; }

    // equals, hashCode, toString ...
}

Klasa record (jedna linijka!):


public record Client(String name, int id) { }

Użycie:


public class Main {
    public static void main(String[] args) {
        Client client = new Client("Ivan", 123);
        System.out.println(client.name()); // Ivan
        System.out.println(client.id());   // 123
        System.out.println(client);        // Client[name=Ivan, id=123]
    }
}

Zwróć uwagę:
- Metody dostępu do pól nazywają się tak samo jak pola: name(), id().
- Nie ma setName() ani setId() — obiektu nie da się zmienić po utworzeniu.

4. Zalety rekordów: mniej kodu, mniej błędów

Mniej kodu — więcej szczęścia

Po co pisać 40 linii, skoro można jedną? Rekordy oszczędzają czas i nerwy, zwłaszcza w dużych projektach, gdzie jest dużo DTO i obiektów wartości.

Niezmienność „z kontraktu”

  • Rekordy są zawsze final i niezmienne.
  • Obiektu nie da się podrobić ani przypadkowo zmienić.
  • Brak „dziwnych” błędów wynikających ze zmiany stanu obiektu w nieoczekiwanym miejscu.
  • Można bezpiecznie używać w programach wielowątkowych (jeśli wszystkie pola też są immutable).

Automatyczna generacja equals/hashCode/toString

Nie trzeba ręcznie pisać metod porównania, obliczania hash i ładnego wypisu. Wszystko dzieje się automatycznie i poprawnie.


Client c1 = new Client("Anna", 42);
Client c2 = new Client("Anna", 42);

System.out.println(c1.equals(c2));                 // true
System.out.println(c1.hashCode() == c2.hashCode()); // true
System.out.println(c1);                             // Client[name=Anna, id=42]

Idealne do kolekcji i kluczy

Obiekty record można używać jako klucze w HashMap, elementy w HashSet itd. — wszystko będzie działać poprawnie, ponieważ equals i hashCode uwzględniają wszystkie pola.


import java.util.HashMap;
import java.util.Map;

Map<Client, String> clients = new HashMap<>();
clients.put(new Client("Anna", 42), "VIP");

System.out.println(clients.get(new Client("Anna", 42))); // VIP

Jawny opis danych

Składnia rekordu od razu pokazuje, jakie dane są przechowywane i że obiekt jest niezmienny. To sprawia, że kod jest bardziej zrozumiały dla innych programistów (i dla was za pół roku).

5. Tabela: porównanie zwykłej klasy i rekordu

Zwykła klasa Klasa record
Składnia Dużo kodu Jedna linijka
Niezmienność Trzeba napisać jawnie Gwarantowana przez kompilator
Autogeneracja metod Nie Tak (equals, hashCode, toString)
Można dodać pola Tak Tylko komponenty rekordu
Dziedziczenie Można dziedziczyć Zawsze final, dziedziczenie niedozwolone
Użycie w kolekcjach Trzeba poprawnie zaimplementować metody Działa „od razu z pudełka”

6. Rozwijamy aplikację edukacyjną: przykład z rekordem

Załóżmy, że w waszej ćwiczebnej aplikacji bankowej trzeba przechowywać operacje na koncie: datę, kwotę, typ operacji (np. „wpłata” lub „wypłata”).

Przed Javą 16:


public class Transaction {
    private final LocalDate date;
    private final double amount;
    private final String type;

    public Transaction(LocalDate date, double amount, String type) {
        this.date = date;
        this.amount = amount;
        this.type = type;
    }

    public LocalDate getDate() { return date; }
    public double getAmount() { return amount; }
    public String getType() { return type; }

    // equals, hashCode, toString ...
}

Od Javy 16 i record:


import java.time.LocalDate;

public record Transaction(LocalDate date, double amount, String type) { }

Użycie:


Transaction t = new Transaction(LocalDate.now(), 100.0, "deposit");
System.out.println(t);         // Transaction[date=2024-06-01, amount=100.0, type=deposit]
System.out.println(t.amount()); // 100.0

7. Wizualizacja: co generuje record

Spójrzmy na „rozłożony” rekord (mniej więcej to, co wygeneruje kompilator):


public final class Point extends java.lang.Record {
    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) { /* porównanie po polach */ }

    @Override
    public int hashCode() { /* obliczanie na podstawie pól */ }

    @Override
    public String toString() { /* ładny wydruk */ }
}

W skrócie: kiedy warto używać record

  • Gdy potrzebny jest niezmienny obiekt z zestawem danych.
  • Gdy potrzebne są „uczciwe” equals/hashCode/toString bez ręcznego pisania.
  • Gdy tworzycie DTO, Value Object, pary, trójki, kolor, punkt, zakres, klucz do kolekcji itp.

8. Typowe błędy przy pracy z rekordami

Błąd nr 1: próba dodania settera lub zmiany pola po utworzeniu.
Rekord nie pozwala zmieniać swoich pól. Jeśli spróbujecie dodać metodę w stylu setX(int x), kompilator od razu powie „niedozwolone”. Tak samo — jeśli spróbujecie zmienić pole bezpośrednio.

Błąd nr 2: próba dodania niestatycznego pola.
W rekordzie można deklarować tylko komponenty (pola wskazane w nawiasach po nazwie rekordu) oraz pola statyczne. Zwykłych niestatycznych pól dodawać nie wolno — kompilator na to nie pozwoli.

Błąd nr 3: używanie rekordu do logiki mutable.
Rekordy nie są przeznaczone do obiektów ze zmiennym stanem. Jeśli trzeba coś zmieniać po utworzeniu — użyjcie zwykłej klasy.

Błąd nr 4: zapomnienie, że rekord zawsze jest final.
Rekordu nie można dziedziczyć i nie można uczynić go nadklasą. Próba naruszenia tego ograniczenia zakończy się błędem kompilacji. Kluczowa wskazówka — nie próbujemy „rozszerzać” rekordu: został zaprojektowany jako kompletny, niezmienny typ.

Błąd nr 5: ignorowanie autogenerowanych metod.
Jeśli nadpisujecie equals, hashCode lub toString, bądźcie ostrożni — nie łamcie ich kontraktu, w przeciwnym razie kolekcje i porównania będą działać niepoprawnie.

1
Zadanie
JAVA 25 SELF, poziom 22, lekcja 0
Niedostępne
Tworzenie cyfrowej karty książki 📚
Tworzenie cyfrowej karty książki 📚
1
Zadanie
JAVA 25 SELF, poziom 22, lekcja 0
Niedostępne
Przegląd danych studenta 🎓
Przegląd danych studenta 🎓
1
Zadanie
JAVA 25 SELF, poziom 22, lekcja 0
Niedostępne
Sprawdzenie tożsamości miast na mapie 🗺️
Sprawdzenie tożsamości miast na mapie 🗺️
1
Zadanie
JAVA 25 SELF, poziom 22, lekcja 0
Niedostępne
Zarządzanie dostępem użytkowników w aplikacji 🔑
Zarządzanie dostępem użytkowników w aplikacji 🔑
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION