Java records – efektívny spôsob správy dátových tried v modernom programovaní

Java bola v minulosti často kritizovaná za to, že treba napísať množstvo kódu, aby program začal niečo robiť. Najväčšie sťažnosti boli smerované k dátovým triedam. V Oracle si toho boli vedomí a začali pracovať na tom, aby Java okrem robustnosti a výrečnosti nadobudla aj atribúty stručnosti a priateľskosti k vývojárom. Vo verzii Java 14 [JEP-359] si autori nachystali ukážku toho, ako by sa dali takéto dátové triedy implementovať a po zapracovaní spätnej väzby od Java komunity priniesli nové rozšírenie jazyka Java vo verzii Java 16 [JEP-395] – Java Records.

Čo je Java Record?

Podobne ako Enum, Record je tiež špeciálny typ triedy v Jave. Records (záznamy) sú určené na zjednodušenie vytvárania nemenných dátových objektov, často označovaných aj ako dátové nosiče. Hlavný rozdiel medzi klasickou triedou a záznamom je v tom, že záznam sa snaží odstrániť celý sprievodný kód, ktorý je potrebný napísať pri nastavení dát alebo pri ich získavaní v inštancii triedy. Java záznam túto zodpovednosť deleguje na Java kompilátor, ktorý automaticky poskytuje stručné implementácie bežných metód ako sú: konštruktor, prístup k členom triedy (getter metódy), hashCode(), equals()toString() metóda.

V skratke povedané, táto významná funkcionalita Java records priniesla možnosť stručnejšie definovať nemeniteľné (immutable) dáta, pričom pomáha znížiť množstvo opakujúceho sa kódu, a tak sa vývojári môžu sústrediť hlavne na programovanie biznis logiky.

Najlepšie pochopíme prínos tohto vylepšenia na príklade. Pozrime sa teraz spolu na to, ako by sme zadefinovali dátový agregát bez Java Record a s ním. Vytvorme si jednoduchú dátovú triedu kniha, s atribútmi názov a cena.

Príklad: Dátová trieda Book bez použitia Record

V tomto príklade, aj keď trieda Book obsahuje len dve polia (name a price), je potrebné zapísať veľa opakujúceho sa kódu (konštruktor, gettery, toString(), equals(), a hashCode()) na zabezpečenie správneho fungovania objektu.

import java.util.Objects;

public class Book {
    private final String name;
    private final double price;

    public Book(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }

    @Override
    public String toString() {
        return "Book {name = '" + name + "', price = " + price + "}";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book book = (Book) o;
        return Double.compare(book.price, price) == 0 && name.equals(book.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, price);
    }
}

Príklad: Dátová trieda Book s použitím Record

So zavedením kľúčového slova record, rovnaká dátová trieda môže byť zadefinovaná oveľa stručnejšie.

public record Book(String name, double price) {}

Táto jedna riadková deklarácia robí v podstate nasledovné:

  • definuje triedu Book s dvoma final atribútmi: name a price,
  • automaticky generuje konštruktor, getter metódy, toString(), equals(), a hashCode() .

Ak sa pozrieme na byte-kód môžeme si všimnúť niekoľko detailov:

  • Kompilátor nahradil kľúčové slovo record slovom class.
  • Kompilátor deklaroval triedu ako final. To znamená, že túto triedu nemožno rozšíriť. Rovnako to znamená, že sa nedá zdediť a je svojou povahou nemenná.
  • Konvertovaná trieda dedí z lang.Record. Všetky záznamy sú podtriedou triedy Record definovanej v balíku java.lang.
  • Existuje parametrizovaný konštruktor pridaný kompilátorom.
  • Kompilátor automaticky vygeneroval metódy toString(), hashCode()equal().
  • Kompilátor pridal metódy na prístup k atribútom triedy. Zhodujú sa s názvami atribútov a neobsahuje žiadne prefixy ako get alebo set.
Trieda Book vytvorená kompilátorom zo záznamu
Trieda Book vytvorená kompilátorom zo záznamu

Použitie v triede Main je podobné na aké sme zvyknutí z používania klasických JavaBean tried. Musíme sa však odnaučiť používať getter metódy, keďže tieto record trieda neobsahuje.

public class Main {
    public static void main(String[] args) {
        Book book1 = new Book("Effective Java", 47.38);
        System.out.println(book1.name());
        System.out.println(book1.price());
        System.out.println(book1);
        Book book2 = new Book("Effective Java", 47.38);
        System.out.println(book1.equals(book2));
    }
}

Výstup tohto programu je nasledovný:

Výstup z programu - Dátová trieda Book s použitím Record

Výhody používania Java Records

Na predchádzajúcom príklade sme videli ako definovaním záznamovej triedy pomocou jedného riadku, sme dokázali jednoducho implementovať jednoduchú dátovú triedu. Zhrňme si preto výhody používania Java Records.

  1. Zredukovanie množstva kódu: Ako bolo ukázané, použitie records eliminuje potrebu manuálne písať množstvo sprievodných, ale potrebných metód. Tieto metódy nezmizli, len sa po novom o ne postará kompilátor.
  2. Nemennosť (Immutability): Dizajnovo sú records nemenné. Po vytvorení objektu nie je možné meniť jeho polia. Toto robí records ideálne pre scenáre, kde je požiadavka na nemennosť (napr. v doménových entitách vo funkcionálnom programovaní alebo distribuovaných systémoch).
  3. Automatizované prístupové metódy: Records automaticky generujú prístupové metódy (gettery) pre každé pole. Namiesto typického getFieldName(), však majú tieto metódy rovnaké názvy ako atribúty. Napríklad v record Book použijeme na prístup k dátam metódy book.name() a book.price().
  4. Zabudovaný kanonický konštruktor: Pri definovaní record sa automaticky vytvorí kanonický konštruktor, ktorý inicializuje všetky polia v zázname.
  5. Jednoduché rozšírenie o nové atribúty: Zmeny v logike nevyžadujú aktualizáciu štandardných metód, ktoré sú generované automaticky a správne kompilátorom. O nové atribúty sa postará kompilátor, ktorý pregeneruje kód a vytvorí novú triedu.
  6. Redukcia chýb: časté chyby, ktoré vznikali napr. pri definovaní metódy equals(), to znamená definovanie logiky kedy sú dve inštancie objektov rovnaké, sú minulosťou. Na správnosť automaticky vygenerovaného kódu kompilátorom sa dá spoľahnúť.

Kedy využiť Java Records

Vo všeobecnosti vieme využívať záznamy v akejkoľvek situácii, kde potrebujeme deklarovať jednoduchý dátový kontajner s vlastnosťami nemeniteľnosti záznamov a chceme využiť automaticky generované metódy. Spomenieme niekoľko scenárov použitia, kedy Java záznamy prídu vhod:

Objekty na prenos dát (DTOs – data transfer objects)

Záznamy môžeme použiť na deklarovanie jednoduchých objektov prenosu údajov, ktoré obsahujú údaje. Je to užitočné pri prenose údajov medzi rôznymi vrstvami aplikácie, napríklad medzi servisnou vrstvou a databázovou vrstvou.

Konfiguračné objekty

Záznamy možno použiť na deklarovanie konfiguračných objektov, ktoré obsahujú sadu konfiguračných vlastností pre aplikáciu alebo modul. Tieto objekty majú zvyčajne nemenné vlastnosti, vďaka čomu sú bezpečné pre vlákna.

Webservice odpovede

Pri vytváraní REST API je bežné vracať dáta vo forme JSON alebo XML. V takýchto prípadoch chceme definovať jednoduchú dátovú štruktúru, ktorá predstavuje odpoveď API. Záznamy sú na to ideálne, pretože umožňujú definovať ľahkú a nemennú dátovú štruktúru, ktorú možno jednoducho serializovať do formátu JSON alebo XML.

Testovacie údaje

Pri písaní unit testov je často potrebné vytvoriť testovacie dáta pre konkrétny testovací scenár. V takýchto prípadoch sú záznamy na to ideálne, pretože testovacie dáta väčšinou bývajú statické a s minimálnym množstvom kódu vieme vytvoriť komplexnejšie sady testov.

Kedy sa vyhnúť používaniu Java Records

Napriek svojim výhodám records plnohodnotne nenahrádzajú pôvodne JavaBean triedy. Preto, ak potrebujeme:

  • Menenie dát po vytvorení
    Ak dáta potrebujú byť zmenené po vytvorení, bude potrebné zmazať starú inštanciu a vytvoriť novú inštanciu triedy alebo bude treba použiť štandardnú triedu namiesto record triedy.
  • Komplexnú biznis logiku
    Aj keď Java record môže obsahovať metódy s biznis logikou, odporúča sa udržiavať záznamy podľa možnosti ako dátové entity a biznis logiku implementovať do separátnych tried, ktoré potom pracujú so záznamami. Ak však tvoja trieda vyžaduje komplexnú logiku nad rámec uchovávania dát, tradičné triedy budú vhodnejšou voľbou.

Záver

Java records nám poskytujú elegantný spôsob, ako modelovať nemenné dátové triedy s minimom kódu. Je ideálna najmä pre triedy, ktoré primárne slúžia na uchovávanie dát, umožňujúc vývojárom písať čistejší a ľahšie udržiavateľný kód. Používaním records, množstvo kódu, ktoré je potrebné napísať delegujeme na kompilátor. Pre scenáre zahŕňajúce mutabilné dáta alebo zložité správanie však máme k dispozícií stále tradičné triedy, ktoré v takých prípadoch zostávajú vhodnejšou voľbou.

O autorovi

Jozef Wagner

Java Developer Senior

Viac ako 10 rokov programujem v Jave, momentálne pracujem v msg life Slovakia ako Java programátor senior a pomáham zákazníkom implementovať ich požiadavky do poistného softvéru Life Factory. Vo voľnom čase si rád oddýchnem v lese, prípadne si zahrám nejakú dobrú počítačovú hru.

Daj nám o sebe vedieť