Records în Java
Începând cu versiunea Java 16, a fost introdusă o nouă funcționalitate numită Records (în română, deseori numite "înregistrări"). Records reprezintă clase destinate să creeze containere de date imutabile și, de asemenea, simplifică dezvoltarea prin reducerea cantității de cod necesar.
Pentru a defini o clasă de tip record, se folosește cuvântul cheie record, urmat de numele clasei și lista de câmpuri între paranteze:
record NumeRecord (camp1, camp2, ...campN){
// corpul record-ului
}
Exemplu:
import java.util.Objects;
public class Program{
public static void main (String[] args){
Person tom = new Person("Tom", 36);
System.out.println(tom.toString());
}
}
class Person {
private final String name;
private final int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
String name() { return name; }
int age() { return age; }
public boolean equals(Object o) {
if (!(o instanceof Person)) return false;
Person other = (Person) o;
return other.name.equals(name) && other.age == age;
}
public int hashCode() {
return Objects.hash(name, age);
}
public String toString() {
return String.format("Person[name=%s, age=%d]", name, age);
}
}
În acest exemplu, clasa Person definește două constante imutabile: name și age.
private final String name;
private final int age;
După ce este creat obiectul Person, valorile acestor câmpuri nu mai pot fi modificate. Sunt disponibile metode pentru a accesa aceste valori, precum name() și age().
String name() { return name; }
int age() { return age; }
Metodele equals(), hashCode() și toString() au fost, de asemenea, suprascrise.
În metoda main, creăm un obiect al clasei Person și afișăm în consolă reprezentarea textuală a acestuia:
Person tom = new Person("Tom", 36);
System.out.println(tom.toString());
În final, consola va afișa:
Person[name=Tom, age=36]
Acum să vedem ce ne oferă Records - vom defini un record, care va fi complet echivalent cu clasa definită mai sus:
public class Program {
public static void main(String[] args) {
Person tom = new Person("Tom", 36);
System.out.println(tom.toString());
}
}
record Person(String name, int age) { }
Records sunt definite folosind cuvântul cheie record, urmat de numele înregistrării. Urmează lista de câmpuri ale înregistrării. În acest caz, sunt definite două câmpuri - name și age. În mod implicit, toate vor fi private și vor avea modificatorul final.
De asemenea, va fi creat un constructor cu doi parametri name și age. Pentru fiecare câmp, va fi creat automat o metodă publică cu același nume pentru a obține valoarea acestui câmp. De exemplu, pentru câmpul name, este creată metoda name(), care returnează valoarea câmpului name.
Vor fi create automat și metodele equals, hashCode și toString. În general, acest record va fi complet echivalent cu clasa definită mai sus, dar conține mult mai puțin cod.
La nevoie, putem apela toate metodele disponibile:
public class Program {
public static void main(String[] args) {
Person tom = new Person("Tom", 36);
System.out.println(tom.name()); // Tom
System.out.println(tom.age()); // 36
System.out.println(tom.hashCode());
Person bob = new Person("Bob", 21);
Person tomas = new Person("Tom", 36);
System.out.println(tom.equals(bob)); // false
System.out.println(tom.equals(tomas)); // true
}
}
record Person(String name, int age) { }
Constructorul unui record
În exemplul de mai sus, am folosit forma de record:
record Person(String name, int age) { }
care de fapt creează constructorul:
Person(String name, int age) {
this.name = name;
this.age = age;
}
Acest constructor este numit "canonic". El primește parametri, care au aceleași nume ca și câmpurile recordului, și atribuie câmpurilor valorile parametrilor corespunzători.
Totuși, la nevoie, putem modifica logica constructorului. De exemplu, ce facem dacă este transmisă o vârstă nevalidă? Putem anticipa această situație prin suprascrierea logicii constructorului:
public class Program {
public static void main(String[] args) {
Person tom = new Person("Tom", -116);
System.out.println(tom.toString());
}
}
record Person(String name, int age) {
Person {
if (age < 1 || age > 110) {
age = 18;
}
}
}
În acest caz, dacă este transmisă o valoare nevalidă, vom folosi o valoare implicită (numărul 18). În final, vom avea un constructor cu următoarea logică:
Person(String name, int age) {
if (age < 1 || age > 110) {
age = 18;
}
this.name = name;
this.age = age;
}
Programul va afișa în consolă următorul rezultat:
Person[name=Tom, age=18]
Putem suprascrie complet constructorul canonic:
public class Program {
public static void main(String[] args) {
Person tom = new Person("Tom", 36);
System.out.println(tom.toString());
System.out.println(tom.name());
}
}
record Person(String name, int age) {
Person(String name, int age) {
if (age < 0 || age > 120) age = 18;
this.name = name;
this.age = age;
}
}
De asemenea, putem defini și alți constructori, dar toți trebuie să apeleze constructorul canonic:
public class Program {
public static void main(String[] args) {
Person tom = new Person("Tom", "Smith", 36);
System.out.println(tom.toString());
}
}
record Person(String name, int age) {
Person(String firstName, String lastName, int age) {
this(firstName + " " + lastName, age);
}
}
Aici este definit un constructor care primește, convențional, prenumele, numele de familie și vârsta utilizatorului. Acest constructor apelează constructorul canonic, transmițându-i valori pentru câmpurile name și age: this(firstName + " " + lastName, age).
Consola va afișa:
Person[name=Tom Smith, age=36]
Suprascrierea metodelor
Putem, de asemenea, să suprascriem metodele pe care le are un record în mod implicit. Acestea sunt metodele equals(), hashCode() și toString(), precum și metodele care au același nume ca și câmpurile înregistrării și returnează valorile corespunzătoare ale câmpurilor.
De exemplu, suprascriem pentru înregistrarea Person metodele toString() și name():
public class Program {
public static void main(String[] args) {
Person tom = new Person("Tom", 36);
System.out.println(tom.toString());
System.out.println(tom.name());
}
}
record Person(String name, int age) {
public String name() { return "Mister " + name; }
public String toString() {
return String.format("Person %s, Age: %d", name, age);
}
}
Consola va afișa:
Person Tom, Age: 36
Mister Tom
Limitări ale records
Trebuie să reținem că nu putem moșteni o înregistrare record din alte clase. De asemenea, nu putem moșteni alte clase din records. Totuși, clasele record pot implementa interfețe. În plus, clasele record nu pot fi abstracte.
În record nu putem defini explicit câmpuri nestatice și inițializatori. Însă putem defini variabile statice și inițializatori, la fel ca metode statice și nestatice:
record Person(String name, int age) {
static int minAge;
static {
minAge = 18;
System.out.println("Static initializer");
}
}