Java Records im Detail
Ein tieferer Einblick in Java Records — kanonische und kompakte Konstruktoren, Validierung und Anwendungsfälle.
Ein Record ist Javas Weg, eine Klasse zu deklarieren, deren einzige Aufgabe das Transportieren von Daten ist. Als Preview in Java 14 eingeführt und in Java 16 finalisiert, reduziert ein Record den üblichen Boilerplate — private finale Felder, einen Konstruktor, Accessoren, equals, hashCode und toString — auf eine einzige Headerzeile. Das frühere Records-Kapitel hat die grundlegende Syntax gezeigt; dieses geht tiefer darauf ein, wie Records tatsächlich funktionieren: ihre kanonischen und kompakten Konstruktoren, wie sie Invarianten durchsetzen, welche Unveränderlichkeitsgarantien Sie erhalten und wo sie passen — und wo nicht.
Was der Compiler für Sie generiert
Wenn Sie record Point(int x, int y) {} schreiben, erzeugt der Compiler eine final-Klasse mit zwei private final-Feldern, einen öffentlichen Konstruktor, der beide entgegennimmt, öffentliche Accessor-Methoden, die genau nach den Komponenten benannt sind (x(), y() — kein get-Präfix), sowie wertbasierte equals, hashCode und toString.
record Point(int x, int y) {}
// Equivalent to (roughly) hand-writing:
// final class Point {
// private final int x;
// private final int y;
// Point(int x, int y) { this.x = x; this.y = y; }
// int x() { return x; }
// int y() { return y; }
// public boolean equals(Object o) { ... compares x and y ... }
// public int hashCode() { ... derived from x and y ... }
// public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
// }Das x und y im Header sind die Komponenten des Records. Die vom Compiler generierten Member werden vollständig aus ihnen abgeleitet, in Deklarationsreihenfolge.
Kanonische und kompakte Konstruktoren
Jeder Record hat einen kanonischen Konstruktor, dessen Parameter den Komponenten entsprechen. Sie schreiben ihn selten vollständig aus — stattdessen verwenden Sie den kompakten Konstruktor, der die Parameterliste und die abschließenden this.field = field-Zuweisungen weglässt. Der Compiler führt Ihren Code zuerst aus und weist dann die (möglicherweise geänderten) Parameter den Feldern zu. Es ist der natürliche Ort für Validierung und Normalisierung.
record Range(int low, int high) {
Range { // compact constructor — no (int low, int high)
if (low > high) {
throw new IllegalArgumentException("low must be <= high");
}
low = Math.max(low, 0); // reassigning the parameter normalizes the field
}
}Wenn Sie jemals die explizite kanonische Form benötigen (zum Beispiel, um eine veränderliche Komponente defensiv zu kopieren), schreiben Sie die vollständige Signatur und führen die Zuweisungen selbst durch:
record Tags(String name, List<String> values) {
Tags(String name, List<String> values) { // explicit canonical constructor
this.name = name;
this.values = List.copyOf(values); // defensive, unmodifiable copy
}
}Unveränderlichkeit und was Records nicht sind
Record-Felder sind final, sodass die Referenz, die jede Komponente hält, sich nach der Konstruktion nie ändert. Das macht Records flach unveränderlich. Aber die Unveränderlichkeit endet bei der Referenz: Wenn eine Komponente auf ein veränderliches Objekt zeigt (wie eine ArrayList), können Aufrufer, die dieses Objekt teilen, seinen Inhalt weiterhin verändern. Defensive Kopien im kanonischen Konstruktor schließen diese Lücke.
| Eigenschaft | Records | Reguläre Klassen |
|---|---|---|
| Felder | immer private final | nach Wahl |
| Klasse | implizit final | erweiterbar, außer final |
| Superklasse | immer java.lang.Record | beliebig (Standard Object) |
| Accessoren | automatisch generiert, kein get-Präfix | manuell geschrieben |
equals/hashCode | wertbasiert, generiert | identitätsbasiert standardmäßig |
| Setter | keine — unveränderlich | erlaubt |
Da ein Record immer java.lang.Record erweitert, kann er keine andere Klasse erweitern. Er kann jedoch weiterhin Interfaces implementieren, statische Member deklarieren und Instanzmethoden hinzufügen.
Verhalten, Statik und Factories hinzufügen
Ein Record ist immer noch eine Klasse. Sie können ihm zusätzliche Methoden, statische Factory-Methoden, statische Felder und sogar verschachtelte Typen geben. Die Komponenten definieren den Zustand; alles andere ist gewöhnliches Java.
record Money(String currency, long cents) {
static Money of(String currency, long cents) { // static factory
return new Money(currency, cents);
}
Money plus(Money other) { // derived behavior
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("currency mismatch");
}
return new Money(currency, cents + other.cents); // returns a new value
}
}Records passen auch natürlich zu Sealed Types und Pattern Matching und modellieren geschlossene Mengen von Datenformen — das Fundament algebraisch orientierten Datendesigns in modernem Java. Ein Sealed Interface legt die Menge erlaubter Record-Implementierungen fest, und ein switch über diese Records kann jedes durch seine Komponenten in einem einzigen Ausdruck dekonstruieren.
Ein durchgearbeitetes Beispiel: Records von Anfang bis Ende
Dieses Programm nutzt die generierten Member eines Records, beweist die Unveränderlichkeit und Klasseneigenschaften via Reflection, erzwingt eine Invariante in einem kompakten Konstruktor, listet die Record-Komponenten in Deklarationsreihenfolge auf und zeigt Records in der Verwendung mit Collections und hinzugefügtem Verhalten.
Was man aus der Ausführung mitnehmen kann:
- Der
Point, für den Sie nie einen Body geschrieben haben, druckte trotzdemPoint[x=3, y=4], antwortete aufa.x()und meldeteequals by value: truemit übereinstimmenden Hash-Codes — der Compiler generierte wertbasiertetoString, Accessoren,equalsundhashCodeallein aus den zwei Komponenten. - Reflection bestätigte den Vertrag, den die Sprache garantiert:
is final class : true(Records können nicht subklassiert werden) undis a record : true(jeder Record erweitertjava.lang.Record), weshalb es keine Setter gibt und die Felder unveränderlich sind. - Der Aufruf
Range(9, 2)wurde mitlow must be <= highabgelehnt. Der kompakte Konstruktor lief bevor die Felder zugewiesen wurden, sodass ein Record niemals in einem ungültigen Zustand konstruiert wird — Validierung gehört dorthin, nicht in eine separate Factory-Prüfung. getRecordComponents()gab die Komponenten in Deklarationsreihenfolge alslow:int high:intzurück und zeigte, dass die Struktur eines Records durch Reflection introspektierbar ist — die Grundlage für Serialisierungsbibliotheken und Frameworks, die Records automatisch abbilden.Money.of("USD", 500).plus(Money.of("USD", 250))produzierteUSD 750, unddistinct()reduzierte zwei identischePoint(0,0)-Werte auf2— Records verhalten sich überall wie echte Werte, auch in Streams und Sets, genau weil ihrequals/hashCodeInhalte vergleicht.
Wann man einen Record verwendet (und wann nicht)
Greifen Sie zu einem Record, wenn der Typ durch seine Daten definiert ist und sich diese Daten nach der Konstruktion nicht ändern:
- DTOs und API-Request-/Response-Payloads.
- Map-Schlüssel und Set-Elemente (wertbasiertes
equals/hashCodeist gratis dabei). - Rückgabetypen, die mehrere Werte bündeln und Wegwerf-Tupel oder Out-Parameter ersetzen.
- Die „Blätter" einer Sealed-Hierarchie, die Sie mit Pattern Matching dekonstruieren.
Bevorzugen Sie eine reguläre Klasse, wenn:
- Das Objekt veränderlichen Zustand oder einen Lebenszyklus hat (Entities, Builder, Services).
- Sie eine andere Klasse erweitern müssen — Records können nur Interfaces implementieren.
- Die Identität des Objekts wichtiger ist als sein Inhalt (Sie wollen Referenzgleichheit).
Ein häufiger Fallstrick: Der Accessor eines Records gibt die gespeicherte Referenz unverändert zurück. Wenn eine Komponente ein veränderlicher Typ ist (eine List, ein Array, ein Date), kopieren Sie sie defensiv im kanonischen Konstruktor — wie das obige Tags-Beispiel mit List.copyOf zeigt — sonst können Aufrufer den „unveränderlichen" Zustand des Records durch die übergebene Referenz mutieren.