W3docs

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.

EigenschaftRecordsReguläre Klassen
Felderimmer private finalnach Wahl
Klasseimplizit finalerweiterbar, außer final
Superklasseimmer java.lang.Recordbeliebig (Standard Object)
Accessorenautomatisch generiert, kein get-Präfixmanuell geschrieben
equals/hashCodewertbasiert, generiertidentitätsbasiert standardmäßig
Setterkeine — unveränderlicherlaubt

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.

java— editable, runs on the server

Was man aus der Ausführung mitnehmen kann:

  • Der Point, für den Sie nie einen Body geschrieben haben, druckte trotzdem Point[x=3, y=4], antwortete auf a.x() und meldete equals by value: true mit übereinstimmenden Hash-Codes — der Compiler generierte wertbasierte toString, Accessoren, equals und hashCode allein aus den zwei Komponenten.
  • Reflection bestätigte den Vertrag, den die Sprache garantiert: is final class : true (Records können nicht subklassiert werden) und is a record : true (jeder Record erweitert java.lang.Record), weshalb es keine Setter gibt und die Felder unveränderlich sind.
  • Der Aufruf Range(9, 2) wurde mit low must be <= high abgelehnt. 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 als low:int high:int zurü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)) produzierte USD 750, und distinct() reduzierte zwei identische Point(0,0)-Werte auf 2 — Records verhalten sich überall wie echte Werte, auch in Streams und Sets, genau weil ihr equals/hashCode Inhalte 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/hashCode ist 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.

Übungen

Übung
Was ermöglicht der kompakte Konstruktor eines Records (z. B. 'Range { ... }'), das ein expliziter Konstruktorrumpf sonst mehr Code erfordern würde?
Was ermöglicht der kompakte Konstruktor eines Records (z. B. 'Range { ... }'), das ein expliziter Konstruktorrumpf sonst mehr Code erfordern würde?
Was this page helpful?