author-avatar
Dominik Martyniak
Java 14 minut

Java Rekord (Records) – Praktyczne wykorzystanie

W Javie 14 został wprowadzony obiekt typu record, zgodnie z JEP 359 . Nowy obiekt ten wnosi do języka wiele praktycznych funkcjonalności, które mogą znacznie ułatwić pracę programistom. Record w Javie 14 automatycznie generuje kod dla często używanych operacji, takich jak hashCode, equals, i toString. Dodatkowo, rekordy oferują prywatne i finalne pola, jednocześnie umożliwiając definiowanie publicznych pól, które są automatycznie generowane przez kompilator Javy.


Dzięki wprowadzeniu recordów, programiści mogą znacząco skrócić i uprościć kod, zwłaszcza w przypadku klas reprezentujących dane, które często wymagają implementacji metod equals, hashCode i toString. Recordy pozwalają na bardziej zwięzłe i czytelne rozwiązania, co przekłada się na zwiększenie produktywności i jakości kodu. Domyślnie posiadamy metody, umożliwiające odczyt pól, odpowiednik getterow.

 

Oczywiście, to dobre pytanie: jak wykorzystać rekordy w praktyce?

 

W poprzedniej części wpisu omówiłem koncepcję rekordów w Javie 14 i jakie korzyści niesie ze sobą ich wprowadzenie. Teraz skupmy się na praktycznym zastosowaniu rekordów, zwłaszcza w kontekście architektury hexagonalnej.

 

W architekturze hexagonalnej (wpis o niej ), często mamy do czynienia z obiektami, które reprezentują domenę problemową. Domena ta jest kluczowym elementem naszej aplikacji i często opiera się na danych, które są niezmienne. W takich przypadkach obiekty typu record idealnie wpasowują się w nasze potrzeby. Wiecej o architekturze w tym wpisie.

Dlaczego zatem rekordy są odpowiednie dla warstwy domeny w architekturze hexagonalnej? Przede wszystkim dlatego, że rekordy są klasami niemutowalnymi, co oznacza, że ich stan nie może być zmieniany po ich utworzeniu. Jest to kluczowe w przypadku reprezentowania danych domenowych, ponieważ chcemy uniknąć nieoczekiwanych zmian w tych danych.

Oczywiście, istnieją sytuacje, w których rekordy nie są odpowiednie, na przykład gdy jednym z pól rekordu jest obiekt lub kolekcja, ponieważ rekordy same w sobie nie zapewniają pełnej ochrony przed modyfikacją tych obiektów. Jednak dla prostych przypadków, w których obiekt rekordu zawiera jedynie typy takie jak long czy String, rekordy są idealnym rozwiązaniem.

 

Przykład implementacji

 

record User(Long userId, String firstName, String lastName, Integer age) {
  }

 

Na pierwszy rzut oka można zauważyć różnicę w deklaracji pól rekordu w porównaniu do klas. W przypadku rekordu, pola są zadeklarowane w nawiasach (), a nie w ciele klasy. Jednak to nie jedyna różnica.

Domyślnie, rekord już generuje konstruktor, który zawiera wszystkie pola zadeklarowane w rekordzie. Oznacza to, że nie musimy ręcznie definiować konstruktora ani implementować getterów dla pól rekordu. Rekord automatycznie dostarcza te funkcjonalności, co sprawia, że nasz kod jest bardziej zwięzły i czytelny.

 

Konstruktor w recordzie ? 

 

W rekordach generowany jest tzw. kompaktowy konstruktor, który umożliwia inicjalizację wszystkich pól rekordu. Jest to wygodne i zwięzłe rozwiązanie, czasami możemy potrzebować przeprowadzić dodatkowe operacje w trakcie tworzenia obiektu. Dlatego w ramach rekordu możemy definiować własne konstruktory. Jednak istnieje pewne ograniczenie: pierwszą instrukcją w takim konstruktorze musi być wywołanie konstruktora kompaktowego rekordu.

record User(Long userId, String firstName, String lastName, Integer age) {
   
    public User(String firstName, String lastName, Integer age) {
        this(null, firstName, lastName, age); // Calling the compact constructor
        // Now we can add custom operations, e.g., validation
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
    }
}

 

W powyższym przykładzie mamy rekord "User", który ma kompaktowy konstruktor, ale również definiujemy własny konstruktor. Pierwszą instrukcją w własnym konstruktorze jest wywołanie konstruktora kompaktowego rekordu za pomocą this(...). Dzięki temu możemy wykonać dodatkowe operacje, takie jak walidacja danych, przed utworzeniem obiektu rekordu. Jest to przydatne rozwiązanie, które daje nam elastyczność w projektowaniu naszych rekordów

 

Nadpisanie konskruktora kompaktowego

 

record User(Long userId, String firstName, String lastName, Integer age) {
    // The compact constructor will be automatically generated

    /**
     * Constructor for the User record that overrides the compact constructor.
     *
     * @param firstName First name of the user.
     * @param lastName  Last name of the user.
     * @param age       Age of the user.
     * @throws IllegalArgumentException If the age is negative.
     */
    public User(String firstName, String lastName, Integer age) {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
    }
}

 

W tym przypadku nadpisujemy kompaktowy konstruktor, a jedyną rzeczą, którą robimy, to sprawdzamy warunek dotyczący wieku i rzucając wyjątek, jeśli jest ujemny. Dzięki temu możemy skoncentrować się na walidacji lub innych operacjach bez potrzeby ponownego przypisywania wartości do pól rekordu. To przydatne podejście, które pozwala na bardziej czytelny i elastyczny kod.

 

Metdoy w rekordach 

 

Oczywiście, możesz dodać metody do rekordu, które wykonują różne operacje na danych rekordu. 

 

public record User(Long userId, String firstName, String lastName, Integer age) {

    public static final long ADULT_AGE = 18;

    /**
     * Constructor for the User record with age validation.
     *
     * @param firstName First name of the user.
     * @param lastName  Last name of the user.
     * @param age       Age of the user.
     * @throws IllegalArgumentException If the age is negative.
     */
    public User(String firstName, String lastName, Integer age) {
        this(null, firstName, lastName, age); // Calling the compact constructor
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
    }

    /**
     * Method to retrieve the full name of the user.
     *
     * @return Full name of the user.
     */
    public String getFullName() {
        return firstName + " " + lastName;
    }

    /**
     * Method to check if the user is an adult.
     *
     * @return True if the user is 18 years or older, false otherwise.
     */
    public boolean isAdult() {
        return age >= ADULT_AGE ;
    }

    /**
     * Method to compare two users based on their age.
     *
     * @param other Another user to compare to.
     * @return 0 if the ages are the same, a negative number if this user is younger than the other, a positive number otherwise.
     */
    public int compareAge(User other) {
        return this.age.compareTo(other.age);
    }
}

 

W powyższym kodzie dodano trzy metody: getFullName(), która zwraca pełne imię i nazwisko użytkownika, isAdult(), która sprawdza, czy użytkownik jest pełnoletni, i compareAge(), która porównuje wiek dwóch użytkowników. Dzięki takim metodom możesz wykonywać bardziej złożone operacje na danych rekordu.

Możesz dodać stałą wartość do rekordu, ale nie jako pole, ponieważ pola rekordu są niezmienne (final) i muszą być zainicjowane w konstruktorze. Możesz jednak dodać pole statyczne do samej klasy, w której znajduje się rekord, jak to:

public static final long ADULT_AGE = 18;

 

Inicjalizacja i działanie na rekordach 

 

public class Main {
    public static void main(String[] args) {
        // Initializing a record object
        User user1 = new User(1L, "John", "Doe", 25);

        // Using accessors
        System.out.println("User's first name: " + user1.firstName());
        System.out.println("User's last name: " + user1.lastName());
        System  .out.println("User's age: " + user1.age());

        // Using methods
        System.out.println("Full name of the user: " + user1.getFullName());
        System.out.println("Is the user an adult? " + user1.isAdult());

        // Another object initialization
        User user2 = new User("Alice", "Smith", 17);

        System.out.println("\nUser's first name: " + user2.firstName());
        System.out.println("User's last name: " + user2.lastName());
        System.out.println("User's age: " + user2.age());
        System.out.println("Full name of the user: " + user2.getFullName());
        System.out.println("Is the user an adult? " + user2.isAdult());
    }
}

W tym przykładzie inicjalizujemy dwa obiekty rekordu "User", a następnie wykorzystujemy metody pól dostępowych, takie jak firstName(), lastName(), i age() które są domyslne dla Recordów, do pobrania danych użytkowników. Dodatkowo, korzystamy z bardziej naszych metod, takich jak getFullName() i isAdult(), które wykonują różne operacje na danych rekordu. To pokazuje, jak łatwo i wygodnie można zarządzać danymi w rekordach i wykorzystywać ich metody.

 

Podsumowanie

 

Rekordy w Javie są specjalnym rodzajem klas wprowadzonym w Java 14 (JEP 359), które służą do reprezentacji danych w sposób zwięzły i niezmienny. Głównym celem rekordów jest ułatwienie tworzenia klas, które przechowują dane i zapewniają dostęp do tych danych za pomocą automatycznie generowanych metod. Oto kilka kluczowych cech rekordów:

Kompaktowy konstruktor: Rekordy automatycznie generują konstruktor, który umożliwia inicjalizację wszystkich pól rekordu. To znacząco upraszcza tworzenie obiektów rekordu.

Pola dostępowe: Rekordy automatycznie generują metody dostępowe (gettery) do pól, które pozwalają na odczyt wartości pól. Pola rekordu są zazwyczaj oznaczone jako final, co oznacza, że nie można ich zmieniać po utworzeniu obiektu rekordu.

Niezmienność: Rekordy są klasami niemutowalnymi, co oznacza, że po utworzeniu obiektu rekordu nie można zmieniać jego stanu. Jeśli chcesz utworzyć nowy obiekt rekordu z innymi danymi, musisz stworzyć nowy obiekt.

Automatyczna metoda equals(): Rekordy automatycznie generują metodę equals(), która porównuje obiekty rekordu na podstawie ich pól. Dwa obiekty rekordu są uważane za równe, jeśli ich pola są równe.

Automatyczna metoda hashCode(): Rekordy generują także metodę hashCode(), która oblicza skrócony kod hash na podstawie pól rekordu. To ułatwia wykorzystanie obiektów rekordu w kolekcjach, takich jak HashSet czy HashMap.

Automatyczna metoda toString(): Rekordy generują domyślną implementację metody toString(), która zwraca reprezentację tekstową obiektu rekordu, opartą na jego polach.

Rekordy są przydatne do reprezentowania danych, takich jak modele domenowe, dane użytkowników itp. Dzięki nim kod staje się bardziej zwięzły, bardziej czytelny i mniej podatny na błędy.

2
[java, java records]

Więcej od Dominik Martyniak

Więcej artykułów