Liigu peamise sisu juurde

Record klass

Sissejuhatus

Javas (ja OOP-s üldisemalt) on laialt levinud muster eraldi klasside loomine andmete kandmise eesmärgil (teisisõnu ka DTO ehk Data Transfer Object). See on klass, mille peamine eesmärk on andmete hoidmine. Sellisel klassil on tavaliselt fikseeritud hulk välju, konstruktor nende initsialiseerimiseks, getterid väärtuste lugemiseks ning üle kirjutatud equals(), hashCode() ja toString() meetodid.

Tüüpiline näide sellisest klassist:

public class Person {
private final int id;
private final String firstName;
private final String lastName;

public Person(int id, String firstName, String lastName) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
}

public int getId() { return id; }
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }

@Override public boolean equals(Object o) { ... }
@Override public int hashCode() { ... }
@Override public String toString() { ... }
}

Kuna antud klassi eesmärk on ainult andmeid hoida, siis seal on palju korduvat ja vähe äriloogikaga seotud koodi. Sellist koodi nimetatakse boilerplate-koodiks.

Alates Java 16-st on võimalik seda probleemi leevendada Record-klassidega. Record on eriotstarbeline klassivorm, mis on mõeldud muutumatute andmekandjate (data carrier) modelleerimiseks.

Deklareerimine ja kasutamine

Record deklareeritakse lühikese ja deklaratiivse süntaksiga:

record Person(int id, String firstName, String lastName) { }

See üks rida kirjeldab kogu andmestruktuuri. Kompilaator genereerib automaatselt:

  • private final väljad iga komponendi jaoks
  • konstruktori, mis vastab kõikidele defineeritud väljadele (Person(int id, String firstName, String lastName))
  • ligipääsumeetodid id(), firstName(), lastName() (ilma get-prefiksita)
  • korrektsed equals(), hashCode() ja toString() implementatsioonid

Kasutamine:

Person p = new Person(1, "Alice", "Smith");

System.out.println(p.id()); // 1
System.out.println(p.firstName()); // Alice
System.out.println(p); // Person[id=1, firstName=Alice, lastName=Smith]

Person q = new Person(1, "Alice", "Smith");
System.out.println(p.equals(q)); // true

Record'i eesmärk ei ole lihtsalt koodi lühendamine, vaid selge semantika: tegemist on muutumatu andmekandjaga, mille identiteet põhineb tema komponentide väärtustel.

Recordid on muutumatud

Kõik recordi väljad on vaikimisi private final väljad. See tähendab, et record on olemuslikult muutumatu. Pärast objekti loomist ei saa selle seisundit muuta.

Settereid ei genereerita ning väljadele ei ole võimalik otse ligi pääseda.

record Person(int id, String firstName, String lastName) { }

Person p = new Person(1, "Alice", "Smith");
// p.id = 10; // Error — field is final

Kui on vaja „muudetud“ versiooni, tuleb luua uus recordi eksemplar uute väärtustega.

Meetodite lisamine

Record võib sisaldada meetodeid nagu tavaline klass. Kuigi see on mõeldud andmekandjaks, võib see sisaldada loogikat, mis on otseselt seotud tema komponentidega.

record Point(int x, int y) {

public double distanceTo(Point other) {
int dx = this.x - other.x;
int dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
}

Kasutamine:

Point a = new Point(0, 0);
Point b = new Point(3, 4);

System.out.println(a.distanceTo(b)); // 5.0

Staatilised tehaseemeetodid

Record võib sisaldada ka static-meetodeid.

Staatilised tehasemeetodid oleme juba eelnevalt läbi käinud. Record'id rakendavad neid samal põhimõttel. Neid kasutatakse sageli nimega konstruktoritena, et muuta objekti loomise viis selgemaks või pakkuda alternatiivseid loomismehhanisme.

Näiteks:

record Temperature(double celsius) {

public static Temperature fromFahrenheit(double fahrenheit) {
return new Temperature((fahrenheit - 32) * 5 / 9);
}

public double toFahrenheit() {
return celsius * 9 / 5 + 32;
}
}

Ning nende kasutamine:

Temperature t = Temperature.fromFahrenheit(98.6);
System.out.println(t.celsius()); // 37.0

Staatiline tehase­meetod võimaldab väljendada loomise semantikat selgemalt kui vaikimisi konstruktor ning võimaldab hoida teisendusloogikat record'i enda sees.

Konstruktorid

Kui recordi loomisel on vaja andmeid valideerida või normaliseerida, saab selleks kasutada kompaktset konstruktorit. See on konstruktor ilma parameetriloeteluta, mis kasutab automaatselt record'i komponente ning võimaldab enne lõplikku omistamist kontrolli teostada.

record Product(String name, double price) {

Product { // compact constructor — no parameter list
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty");
}
if (price < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
}
}

Kasutamine:

Product valid   = new Product("Keyboard", 49.99);   // OK
Product invalid = new Product("", 10.0); // IllegalArgumentException

Kompaktne konstruktor võimaldab lisada valideerimisloogika ilma, et peaks ise kõiki välju käsitsi omistama. Kompilaator genereerib vastava omistamiskoodi automaatselt.

Lisaks on võimalik record'ile luua ka muid konstruktoreid. Iga loodud konstruktor peab lõpuks välja kutsuma tavalist konstruktorit ühel või teisel viisil, kasutades this(...) käsklust. Kui lisakonstruktor ei kutsu tavalist konstruktorit välja, tekib kompileerimisviga.

record Person(int id, String firstName, String lastName) {

// Non-canonical constructor — must delegate to the canonical one
public Person(String firstName, String lastName) {
this(0, firstName, lastName);
}
}
Person alice = new Person(1, "Alice", "Smith");   // canonical — all fields provided
Person bob = new Person("Bob", "Jones"); // non-canonical — id defaults to 0

See reegel kehtib, sest kanooniline konstruktor on ainus koht, kus recordi väljad tegelikult omistatakse. Kõik teised konstruktorid peavad selle kaudu läbima.

Recordid ja liidesed

Samuti võivad record'id implementeerida liideseid. See muudab recordid loomulikuks valikuks olukordades, kus on vaja muutumatut andmetüüpi, mis osaleb suuremas tüübihierarhias või täidab kindlat lepingut.

interface Shape {
double area();
}

record Circle(double radius) implements Shape {
@Override
public double area() {
return Math.PI * radius * radius;
}
}

record Rectangle(double width, double height) implements Shape {
@Override
public double area() {
return width * height;
}
}
List<Shape> shapes = List.of(new Circle(5.0), new Rectangle(3.0, 4.0));
for (Shape s : shapes) {
System.out.println(s.area());
}
// 78.53981633974483
// 12.0

Antud juhul toimivad Circle ja Rectangle nii muutumatute andmekandjatena kui ka Shape-lepingu realiseerijatena.

Mida recordid ei saa teha

Record'itel on teadlikult seatud piirangud, mis rõhutavad nende rolli lihtsate ja muutumatute andmekandjatena:

  • Nad ei saa laiendada teist klassi (iga record vaikimisi laiendab java.lang.Record klassi).
  • Nad ei saa deklareerida täiendavaid isendivälju väljaspool record'i päist. Kogu olek peab olema määratletud komponentidena päises.
  • Nad ei saa olla abstraktsed ega omada alamklasse (record on vaikimisi final).

Küll aga võivad recordid implementeerida suvalise arvu liideseid.

nõuanne

Kasuta recordit siis, kui klassi eesmärk on andmete kandmine ning muutumatus on soovitud omadus. Kasuta tavalist klassi siis, kui vajad muutuvat olekut, pärilushierarhiat või keerukamat initsialiseerimisloogikat.