Java Records
Java Records erstellen kompakte, unveränderliche Datenklassen mit automatisch generierten Accessors, equals und hashCode.
Ein Record ist eine Klasse, deren einzige Aufgabe es ist, Daten zu tragen. Sie deklarieren die Felder einmal im Header, und der Compiler generiert den Konstruktor, die Accessors, equals, hashCode und toString für Sie. Was früher 40 Zeilen Getter und Boilerplate erforderte, ist jetzt eine einzige Zeile:
public record Point(int x, int y) {}Das ist die gesamte Klasse. new Point(3, 4), p.x(), p.y(), p.equals(other) und p.toString() funktionieren alle und verhalten sich genau so, wie man es erwarten würde.
Records wurden in Java 16 finalisiert (nach Vorschauen in 14 und 15). Diese Seite behandelt, was der Compiler generiert, wie man einen Record validiert und anpasst, die Regeln, die Records durchsetzen, und wie Records mit Pattern Matching zusammenarbeiten.
Warum Records existieren
Vor Records benötigte jede „Datenklasse" ein privates finales Feld pro Komponente, einen Konstruktor, der sie zuweist, einen Getter pro Feld, wertbasiertes equals und hashCode sowie ein toString. Der größte Teil dieses Codes war mechanisch und leicht auf subtile Weise fehlerhaft (ein Feld in equals vergessen, das falsche Feld aus einem Getter zurückgeben, einen Tippfehler hineinkopieren). Records reduzieren all das auf einen Header und eliminieren sowohl den Schreibaufwand als auch die Fehler.
Komponenten und Accessors
Die Argumente im Header werden Komponenten genannt. Jede wird zu:
- Einem
private final-Feld mit dem gleichen Namen. - Einer Accessor-Methode mit dem gleichen Namen (nicht
getX()— einfachx()).
Point p = new Point(3, 4);
System.out.println(p.x() + ", " + p.y()); // 3, 4Die Namen stimmen überein, weil es bei Records darum geht, Daten offen zugänglich zu machen. Es gibt kein get-Präfix, weil es nichts zu verbergen gibt.
Generiertes equals, hashCode, toString
Zwei Records sind gleich, wenn sie denselben Record-Typ haben und jede Komponente gleich ist:
Point a = new Point(3, 4);
Point b = new Point(3, 4);
System.out.println(a.equals(b)); // true
System.out.println(a); // Point[x=3, y=4]hashCode kombiniert alle Komponenten, sodass Records ohne zusätzlichen Aufwand korrekt als Schlüssel in HashMap und HashSet funktionieren.
Kompakter Konstruktor
Jeder Record hat einen kanonischen Konstruktor — denjenigen, dessen Parameter die Komponenten der Reihe nach entsprechen. Standardmäßig schreibt der Compiler ihn für Sie (er weist einfach jeden Parameter seinem Feld zu). Wenn Sie Validierung oder Normalisierung benötigen, müssen Sie diese Zuweisungen nicht wiederholen: Schreiben Sie einen kompakten Konstruktor, der keine Parameterliste hat und vor den impliziten Feldzuweisungen ausgeführt wird:
public record Range(int low, int high) {
public Range {
if (low > high)
throw new IllegalArgumentException("low > high");
}
}Sie können die Parametervariablen auch innerhalb des kompakten Konstruktors neu zuweisen — die endgültigen Werte sind das, was die Felder erhalten:
public record Name(String first, String last) {
public Name {
first = first.strip();
last = last.strip();
}
}Methoden hinzufügen
Records können beliebige Methoden haben, die Sie normalerweise schreiben würden — sie können jedoch keine zusätzlichen Instanzfelder haben (alles, was den Record unterstützt, muss eine Komponente sein):
public record Point(int x, int y) {
public double distanceTo(Point other) {
int dx = x - other.x;
int dy = y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
}Statische Felder und statische Methoden sind zulässig. Records können auch Interfaces implementieren.
Was Records nicht können
- Keine Vererbung. Ein Record erweitert implizit
java.lang.Recordund istfinal— Sie können einen Record nicht erweitern, und ein Record kann keine andere Klasse erweitern. - Kein veränderbarer Zustand. Alle Komponenten sind
final. Wenn Sie Mutation benötigen, verwenden Sie eine normale Klasse. - Keine Instanzfelder außerhalb von Komponenten. Sie können kein zusätzliches
private int cache;hinzufügen.
Diese Einschränkungen sind der eigentliche Sinn. Ein Record verspricht: „Ich bin nur meine Komponenten, und nichts weiter." Dieses Versprechen macht equals, hashCode und Serialisierung sicher für die automatische Generierung.
Wann man einen Record verwenden sollte
Records sind richtig, wenn Sie sonst eine kleine unveränderliche Datenklasse schreiben würden — DTOs, Konfigurationsobjekte, Rückgabetypen, die ein paar Werte zusammenfassen, Tupel in Pattern-Matching-Switches. Sie sind falsch, wenn der Typ eine Identität hat, veränderbaren Zustand besitzt oder die Wurzel einer Hierarchie ist.
Ein ausgearbeitetes Beispiel
Records im Pattern Matching
Da die Komponenten eines Records öffentlich und geordnet sind, weiß der Compiler auch, wie er einen Record auseinandernehmen kann. Ein Record-Pattern bindet jede Komponente in einem Schritt an eine Variable, was Records zur natürlichen Form für Daten macht, über die man per switch verzweigt:
sealed interface Shape permits Circle, Rect {}
record Point(int x, int y) {}
record Circle(Point center, double r) implements Shape {}
record Rect(Point a, Point b) implements Shape {}
static String describe(Shape s) {
return switch (s) {
case Circle(Point c, double r) -> "circle r=" + r + " at " + c.x() + "," + c.y();
case Rect(Point a, Point b) -> "rect " + a + " to " + b;
};
}Das Pattern Circle(Point c, double r) prüft, ob s ein Circle ist, und extrahiert seine Komponenten in einem einzigen Ausdruck. Siehe Java Pattern Matching für das vollständige Feature, einschließlich verschachtelter Patterns.
Was als Nächstes kommt
Records beschränken eine Klasse auf eine feste Menge von Daten. Das nächste Kapitel führt Sealed Classes ein, die eine Hierarchie auf eine feste Menge von Untertypen beschränken — das fehlende Stück zur Modellierung geschlossener algebraischer Datentypfamilien in Java. Weiter zu Java Sealed Classes.