record în Java

record declară o clasă imutabilă pentru date: constructorul, accesorii, equals, hashCode și toString sunt generați automat.

Sintaxă
record Punct(int x, int y) { }

record Produs(String nume, double pret) {
    Produs {                         // constructor compact: validare
        if (pret < 0) throw new IllegalArgumentException();
    }
    double cuTva() { return pret * 1.21; }
}

Ce face record

Un record este o clasă scurtă pentru obiecte care doar poartă date. Din record Punct(int x, int y) compilatorul generează: câmpuri private final, constructorul cu toți parametrii, metodele de acces x() și y() (fără prefixul get), plus equals, hashCode și toString bazate pe toate componentele.

Un record este imutabil și final: nu poate fi extins, iar câmpurile nu se pot schimba după construcție. Pentru o „modificare” creezi un record nou. Poți adăuga metode, câmpuri și metode static, constructori suplimentari și poți implementa interfețe, dar nu poți declara alte câmpuri de instanță.

Constructorul compact (Produs { ... }, fără paranteze) rulează înaintea atribuirilor automate și este locul pentru validări sau normalizări. Records merg foarte bine ca chei în HashMap, ca rezultate cu mai multe valori și în record patterns.

Exemple

Exemplul 1: Ce primești gratuit

Accesorii au numele componentelor. toString afișează toate valorile, iar equals compară conținutul: două puncte cu aceleași coordonate sunt egale.

record Punct(int x, int y) { }

public class Main {
    public static void main(String[] args) {
        Punct a = new Punct(3, 4);
        Punct b = new Punct(3, 4);
        System.out.println(a);
        System.out.println("x = " + a.x() + ", y = " + a.y());
        System.out.println("a.equals(b): " + a.equals(b));
        System.out.println("a == b: " + (a == b));
        System.out.println("hashCode egal: " + (a.hashCode() == b.hashCode()));
    }
}

Rezultat

Punct[x=3, y=4]
x = 3, y = 4
a.equals(b): true
a == b: false
hashCode egal: true

Exemplul 2: Constructor compact, metode și „modificare” prin copie

Constructorul compact elimină spațiile din nume și refuză prețul negativ. cuPret nu schimbă obiectul, ci returnează un record nou.

record Produs(String nume, double pret) {
    Produs {
        nume = nume.strip();
        if (pret < 0) {
            throw new IllegalArgumentException("preț negativ: " + pret);
        }
    }

    double pretCuTva() {
        return Math.round(pret * 1.21 * 100) / 100.0;
    }

    Produs cuPret(double pretNou) {
        return new Produs(nume, pretNou);
    }
}

public class Main {
    public static void main(String[] args) {
        Produs p = new Produs("  Telemea de oaie ", 38.0);
        System.out.println(p + " -> cu TVA: " + p.pretCuTva());

        Produs redus = p.cuPret(32.5);
        System.out.println("redus: " + redus + ", original: " + p);

        try {
            new Produs("Brânză", -5);
        } catch (IllegalArgumentException e) {
            System.out.println("refuzat: " + e.getMessage());
        }
    }
}

Rezultat

Produs[nume=Telemea de oaie, pret=38.0] -> cu TVA: 45.98
redus: Produs[nume=Telemea de oaie, pret=32.5], original: Produs[nume=Telemea de oaie, pret=38.0]
refuzat: preț negativ: -5.0

Exemplul 3: Record ca cheie și ca rezultat multiplu

Statistici întoarce trei valori dintr-o metodă. Locatie este cheie într-un TreeMap, comparată prin Comparable.

import java.util.Map;
import java.util.TreeMap;

record Statistici(double minim, double maxim, double medie) { }

record Locatie(String judet, String oras) implements Comparable<Locatie> {
    @Override
    public int compareTo(Locatie alta) {
        int c = judet.compareTo(alta.judet);
        return c != 0 ? c : oras.compareTo(alta.oras);
    }
}

public class Main {
    static Statistici calculeaza(double... note) {
        double min = note[0], max = note[0], suma = 0;
        for (double n : note) {
            min = Math.min(min, n);
            max = Math.max(max, n);
            suma += n;
        }
        return new Statistici(min, max, suma / note.length);
    }

    public static void main(String[] args) {
        Statistici s = calculeaza(9.5, 7.0, 8.25, 10.0);
        System.out.println(s);
        System.out.println("media: " + s.medie());

        Map<Locatie, Integer> clienti = new TreeMap<>();
        clienti.merge(new Locatie("Iași", "Pașcani"), 1, Integer::sum);
        clienti.merge(new Locatie("Cluj", "Turda"), 1, Integer::sum);
        clienti.merge(new Locatie("Iași", "Pașcani"), 1, Integer::sum);
        System.out.println(clienti);
    }
}

Rezultat

Statistici[minim=7.0, maxim=10.0, medie=8.6875]
media: 8.6875
{Locatie[judet=Cluj, oras=Turda]=1, Locatie[judet=Iași, oras=Pașcani]=2}

Greșeli frecvente

  • Apelezi p.getNume(), ca la clasele obișnuite. Accesorul are numele componentei: p.nume().
  • Încerci să modifici un câmp: cannot assign a value to final variable pret. Creează un record nou cu valoarea schimbată.
  • Pui un array sau o listă modificabilă într-un record și crezi că totul e imutabil. Doar referința e finală; copiază colecția în constructorul compact (lista = List.copyOf(lista);).

Comenzi înrudite

  • Record patterns: Un record pattern verifică tipul unui record și îi desface direct componentele în variabile, inclusiv pentru records imbricate.
  • Clase și obiecte (class): O clasă (cuvântul cheie class) descrie un tip nou: câmpuri pentru date și metode pentru comportament; obiectele sunt instanțele ei.
  • equals și hashCode: equals decide când două obiecte sunt „egale” ca valoare; hashCode trebuie rescris împreună cu el, altfel HashMap și HashSet nu funcționează corect.
  • sealed, permits și non-sealed: O clasă sau interfață sealed își alege explicit subtipurile (permits); fiecare subtip este final, sealed sau non-sealed.
  • final (și static final): final interzice schimbarea: o variabilă primește valoare o singură dată, o metodă nu poate fi rescrisă, o clasă nu poate fi extinsă.