java recordsjava records
HappyCoders Glasses

Java Records

Sven Woltmann
Sven Woltmann
Aktualisiert: 21. Mai 2024

Records sind eine von zwei großen Neuerungen in Java 16 (die zweite ist „Pattern Matching for instanceof“). In diesem Artikel erfährst du:

  • Was sind Java Records, und wofür brauchen wir sie?
  • Wie implementiert und benutzt man Records in Java?
  • Wie kann man einen Java Record um zusätzliche Funktionen erweitern?
  • Was ist im Zusammenhang mit der Vererbung wichtig?
  • Was ist bei der Serialisierung und Deserialisierung von Records zu beachten?
  • Wozu braucht man Records, wenn man dessen Komponenten auch von der IDE ... oder von Lombok erzeugen lassen kann?

Fangen wir mit einem Beispiel aus den Zeiten vor Records an...

Wofür brauchen wir Records?

Nehmen wir an, wir wollen eine unveränderliche Klasse Point erstellen, mit x- und y-Koordinaten und allem, was man braucht, um diese Klasse sinnvoll einzusetzen. Wir wollen Point-Objekte erstellen, deren Felder auslesen und sie in Sets speichern oder als Keys in Maps verwenden können.

Heraus käme dabei in etwa der folgende Code:

public class Point {
  private final int x;
  private final int y;

  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }

  public int getX() {
    return x;
  }

  public int getY() {
    return y;
  }

  @Override
  public boolean equals(Object obj) {
    if (obj == this) return true;
    if (obj == null || obj.getClass() != this.getClass()) return false;
    Point that = (Point) obj;
    return this.x == that.x && this.y == that.y;
  }

  @Override
  public int hashCode() {
    return Objects.hash(x, y);
  }

  @Override
  public String toString() {
    return "Point[x=%d, y=%d]".formatted(x, y);
  }
}Code-Sprache: Java (java)

Das ist schon eine ganze Menge Boilerplate-Code für die Anforderung „eine Klasse mit x- und y-Werten“.

Wer Lombok in seinen Projekten einsetzen wollte und durfte, war klar im Vorteil. Lombok kann Konstruktoren, Getter, equals()-, hashCode()- und toString()-Methoden automatisch erstellen. Der Code reduziert sich dadurch auf wenige Zeilen:

@AllArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class Point {
  private final int x;
  private final int y;
}Code-Sprache: Java (java)

Das ist schon deutlich komfortabler. Lombok ist ausgereift und integriert sich nahtlos in fast jede IDE. Ich setze es seit über zehn Jahren immer wieder gerne ein.

Seit Java 16 geht es allerdings noch kürzer:

public record Point(int x, int y) {}
Code-Sprache: Java (java)

Mit Records wird aus den ursprünglich 22 Zeilen – bzw. den 7 Zeilen mit Lombok – nur noch eine Zeile! Das ist nicht nur kürzer, sondern auch sicherer (s. Abschnitte Java Records vs. Klassen und Java Records vs. Lombok).

Schauen wir uns im Folgenden an, wie genau man Records schreibt und wie man sie nutzt.

Wie implementiert und benutzt man Records in Java?

Im vorherigen Abschnitt haben wir gesehen, wie man einen Record mit nur einer Zeile Code schreibt:

public record Point(int x, int y) {}Code-Sprache: Java (java)

Der Compiler generiert daraus die Klasse Point mit:

  • den finalen Felder int x und int y (den sogenannten „Komponenten“ des Records),
  • einem Konstruktor, der beide Felder setzt (dem sogenannten „kanonischen Konstruktor“),
  • den Accessor-Methoden x() und y() zum Lesen der Komponenten,
  • einer equals()-Methode, die zwei Point-Instanzen als gleich einstuft, wenn deren x- und y-Koordinaten gleich sind,
  • einer hashCode()-Methode, die für zwei gleiche Point-Instanzen den gleichen Hashwert liefert (im Point-Beispiel wird der Hashcode als x * 31 + y berechnet),
  • einer toString()-Methode, die einen lesbaren Text liefert (im Beispiel „Point[x=..., y=...]“).

Du kannst Point wie eine reguläre Klasse einsetzen (in der ersten Zeile des folgenden Code-Beispiels wird der oben erwähnte, automatisch generierte „kanonische Konstruktor“ aufgerufen):

Point p = new Point(5, 10);
int x = p.x();
int y = p.y();Code-Sprache: Java (java)

Vergleichen kannst du zwei Punkte dann bspw. wie folgt:

Point p1 = new Point(8, 4);
Point p2 = new Point(4, 3);
if (p1.equals(p2)) {
  // ...
}Code-Sprache: Java (java)

Java Record Konstruktoren

Im vorangegangenen Abschnitt hast du gelernt, dass der Compiler automatisch einen Konstruktor erzeugt, den sogenannten kanonischen Konstruktor. In diesem Kapitel erfährst du, wie du diesen kanonischen Konstruktor überschreiben kannst, wie du sogenannte „kompakte“ Konstruktoren schreiben kannst – und beliebige weitere nicht-kanonische Konstruktoren.

Kanonischen Konstruktor eines Records überschreiben

Wir können den kanonischen Konstruktor eines Records auch selbst implementieren:

public record Point(int x, int y) {
  /** Canonical constructor as the compiler would generate it */
  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }
}Code-Sprache: Java (java)

Sinn macht das aber erst, wenn wir vor oder nach der Zuweisung der Record-Felder zusätzlichen Code ausführen – z. B. könnten wir sicherstellen wollen, dass die Koordinaten nicht negativ sind:

public record Point(int x, int y) {
  /** Canonical constructor */
  public Point(int x, int y) {
    if (x < 0 || y < 0) throw new IllegalArgumentException();

    this.x = x;
    this.y = y;
  }
}Code-Sprache: Java (java)

Neben der Validierung könnten wir auch Parameter transformieren oder z. B. eine defensive Kopie eines Arrays erzeugen.

Wichtig ist bei dieser Form, dass die Konstruktor-Signatur exakt die gleiche ist wie die des Records. Folgendes ist hingegen nicht erlaubt:

public record Point(int x, int y) {
  public Point(int a, int b) {  // Other names than x and y are not allowed!
    this.x = a;
    this.y = b;
  }
}Code-Sprache: Java (java)

Der Compiler würde das mit folgender Fehlermeldung quittieren:

$ javac Point.java
Point.java:4: error: invalid canonical constructor in record Point
    public Point(int a, int b) {
           ^
  (invalid parameter names in canonical constructor)
1 error
Code-Sprache: Klartext (plaintext)

Ebenso wichtig ist, dass alle Felder gesetzt werden (logisch, sie sind ja final). Würden wir nur x setzen, nicht aber y, dann würde der Compiler mit folgender Meldung abbrechen:

$ javac Point.java
Point.java:4: error: variable y might not have been initialized
    }
    ^
1 error
Code-Sprache: Klartext (plaintext)

Interessanterweise muss keine 1:1-Zuordnung der Konstruktor-Parameter zu den Felder erfolgen. Es müssen nicht einmal alle Parameter verwendet werden. So ist auch folgender Code gültig:

public record Point(int x, int y) {
  public Point(int x, int y) {
    this.x = x;
    this.y = x;  // Assigning this.y to x here - and ignoring y
  }
}Code-Sprache: Java (java)

Glücklicherweise erkennen moderne IDEs das. IntelliJ z. B. warnt hier mit „'x' should probably not be assigned to 'y'“.

Schließlich darf die Sichtbarkeit des kanonischen Konstruktors nicht restriktiver sein als die Sichtbarkeit des Records selbst. Das bedeutet, dass ein als private gekennzeichneter Record einen als public markierten Konstruktor haben kann – ein als public deklarierter Record darf andersherum jedoch keinen privaten Konstruktor haben – folgendes ist also nicht erlaubt:

public record Point(int x, int y) {
  private Point(int x, int y) {  // private constructor not allowed for public record
    this.x = x;
    this.y = y;
  }
}Code-Sprache: Java (java)

Der Compiler würde das mit der folgenden Fehlermeldung quittieren:

javac Point.java
Point.java:2: error: invalid canonical constructor in record Point
  private Point(int x, int y) {
          ^
  (attempting to assign stronger access privileges; was public)
1 errorCode-Sprache: Klartext (plaintext)

Kompakter Konstruktor („Compact Constructor“)

Es gibt noch eine prägnantere Variante, um den kanonischen Konstruktor zu überschreiben. Man kann die Parameter in der Signatur und die Zuweisungen komplett weglassen. Diese Art wird kompakter Konstruktor („compact constructor“) genannt:

public record Point(int x, int y) {
  /** Compact constructor */
  public Point { // ← No parameters here
    if (x < 0 || y < 0) throw new IllegalArgumentException();
    // ← No assignments here
  }
}Code-Sprache: Java (java)

Der Compiler fügt die Zuweisungen this.x = x und this.y = y automatisch am Ende des Konstruktors ein und erzeugt damit letztendlich aus dem kompakten Konstruktor exakt denselben Bytecode wie aus dem im vorherigen Abschnitt als zweites gezeigten kanonischen Konstruktor.

Die Paramter dürfen innerhalb des Konstruktors auch verändert werden, z. B. könnten wir stillschweigend alle negativen Werte durch 0 ersetzen:

public record Point(int x, int y) {
  /** Compact constructor */
  public Point {
    x = Math.max(x, 0);
    y = Math.max(y, 0);
  }
}Code-Sprache: Java (java)

Das würde dem folgenden kanonischen Konstruktor entsprechen:

public record Point(int x, int y) {
  /** Canonical constructor */
  public Point(int x, int y) {
    x = Math.max(x, 0);
    y = Math.max(y, 0);
    this.x = x;
    this.y = y;
  }
}Code-Sprache: Java (java)

Beide Formen des Konstruktors sind letztendlich gleich, und es darf nur entweder ein kanonischer oder ein kompakter Konstruktor implementiert werden.

Meine Empfehlung ist es, immer einen kompakten Konstruktor zu verwenden. Schließlich wollen wir Programmiererinnen und Programmierer unsere Ideen zum Ausdruck bringen – und nicht unnötigen Boilerplate-Code schreiben.

Moderne IDEs wie IntelliJ können mit einem Klick einen kanonischen Konstruktor in einen kompakten Konstruktor umwandeln – und umgekehrt.

Achtung: Da die Komponenten des Records erst am Ende des Konstruktors gesetzt werden, sollte man nicht innerhalb des Konstruktors auf die Accessor-Methoden – im Beispiel x() und y() – zugreifen. Die Komponenten sind zu diesem Zeitpunkt noch mit Default-Werten belegt (im Fall von int also mit 0).

Man sollte stattdessen auf die (nicht explizit angegebenen) Konstruktor-Parameter zugreifen:

public record Point(int x, int y) {
  /** Compact constructor */
  public Point {
    System.out.println(x());  // Prints 0 (fields are not yet assigned)
    System.out.println(x);    // Prints the x parameter passed to the constructor
  }
}Code-Sprache: Java (java)

Deutlich wird das, wenn man innerhalb des Konstruktors versucht über this auf die Felder zuzugreifen:

public record Point(int x, int y) {
  /** Compact constructor */
  public Point {
    System.out.println(this.x);  // Not allowed - x is not yet initialized
  }
}
Code-Sprache: Java (java)

Dieser Code führt zu einem Compilerfehler:

javac Point.java
Point.java:3: error: variable x might not have been initialized
    System.out.println(this.x);
                           ^
1 error
Code-Sprache: Klartext (plaintext)

Konsistent wäre es gewesen, auch den Zugriff über x() und y() im Konstruktor zu verbieten.

Zusätzliche Konstruktoren in Records

Records können um zusätzliche Konstruktoren erweitert werden, wie z. B. einen Default-Konstruktor (einen ohne Parameter) oder einen, der x und y auf denselben Wert setzt:

public record Point(int x, int y) {
  /** Default constructor */
  public Point() {
    this(0, 0);
  }

  /** Custom constructor */
  public Point(int value) {
    this(value, value);
  }
}Code-Sprache: Java (java)

Dabei müssen immer a) alle Felder gesetzt werden und b) muss das, wie im vorangegangenen Beispiel gezeigt, durch Delegation an den kanonischen Konstruktor per this(...) geschehen.

Die Werte direkt zuzuweisen, wie in folgendem Code, ist hingegen erlaubt:

public record Point(int x, int y) {
  /** Default constructor */
  public Point() {
    this.x = 0;  // Not allowed!
    this.y = 0;  // Not allowed!
  }

  /** Custom constructor */
  public Point(int value) {
    this.x = value;  // Not allowed!
    this.y = value;  // Not allowed!
  }
}Code-Sprache: Java (java)

Der Grund dafür ist, dass immer – egal welchen Konstruktor man verwendet – die möglicherweise im kanonischen oder kompakten Konstruktor implementierten Parameter-Validierungen aufgerufen werden sollen.

Statische Felder in Records

Records können um statische Felder (finale sowie nicht-finale) erweitert werden. Beispielsweise könnten wir die 0 aus dem oben gezeigten Default-Konstruktor in eine Konstante extrahieren:

public record Point(int x, int y) {
  private static final int ZERO = 0;

  public Point() {
    this(ZERO, ZERO);
  }
}Code-Sprache: Java (java)

Wir könnten z. B. auch einen statischen Instanz-Counter hinzufügen, der im Konstruktor hochgezählt wird:

public record Point(int x, int y) {
  private static long instanceCounter = 0;

  public Point {
    synchronized (Point.class) {
      instanceCounter++;
    }
  }
}Code-Sprache: Java (java)

Tatsächlich würde ich so einen Counter eher als AtomicLong oder LongAdder implementieren – die wären dann allerdings wieder final und damit nicht als Beispiel für ein nicht-finales statischen Feld geeignet. ;-)

Methoden in Records

Genau wie den kanonischen Konstruktor können wir auch die automatisch generierten Accessor-Methoden eines Records überschreiben. Der folgende Record enthält eine Array-Komponente und erzeugt im Konstruktor und im Accessor jeweils eine defensive Kopie des Arrays, um Änderungen am im Record gespeicherten Array zu verhindern:

public record ImmutableArrayHolder(int[] array) {
  /* Compact constructor */
  public ImmutableArrayHolder {
    array = array.clone();
  }

  /* Accessor method */
  public int[] array() {
    return array.clone();
  }
}Code-Sprache: Java (java)

Neben zusätzlichen Konstruktoren und statischen Feldern lassen sich in Java-Records auch zusätzliche statische und nicht-statische Methoden definieren.

Die folgende statische Methode gibt den Wert des Instanz-Zählers zurück:

public record Point(int x, int y) {
  private static long instanceCounter = 0;

  // ... Constructor increasing instanceCounter ...

  public static synchronized long getInstanceCounter() {
    return instanceCounter;
  }
}Code-Sprache: Java (java)

Um Point threadsicher zu machen, wird jeglicher Zugriff auf instanceCounter synchronisiert. Das schließt auch den Getter mit ein, um sicherzustellen, dass wir nicht einen im CPU-Core-Cache zwischengespeicherten Wert erhalten, sondern immer den aktuellen Wert aus dem Hauptspeicher.

Eine nicht-statische, also eine Instanz-Methode, könnten wir z. B. implementieren, um den euklidischen Abstand zu einem anderen Punkt zu berechnen:

public record Point(int x, int y) {
  public double distanceTo(Point target) {
    int dx = target.x() - this.x();
    int dy = target.y() - this.y();
    return Math.sqrt(dx * dx + dy * dy);
  }
}Code-Sprache: Java (java)

Aufrufen können wir die Methode z. B. wie in folgendem Beispiel:

Point p1 = new Point(17, 3);
Point p2 = new Point(18, 12);
double distance = p1.distanceTo(p2);
Code-Sprache: Java (java)

Bei der Implementierung und dem Aufruf von Record-Methoden gibt es keinen Unterschied zu normalen Klassen.

Records und Vererbung

Records können Interfaces implementieren:

public interface WithXCoordinate {
  int x();
}

public record Point(int x, int y) implements WithXCoordinate {}Code-Sprache: Java (java)

Das ist auch in Kombination mit den in Java 17 veröffentlichten Sealed Types möglich:

public interface WithXCoordinate permits Point, Point3D {
  int x();
}

public record Point(int x, int y) implements WithXCoordinate {}

public record Point(int x, int y, int z) implements WithXCoordinate {}Code-Sprache: Java (java)

Records können hingegen nicht von Klassen erben. Das folgende ist also nicht erlaubt:

public class TaggedElement {
  private String tag;
}

public record Point(int x, int y) extends TaggedElement {}  // Not allowed!Code-Sprache: Java (java)

Das liegt daran, dass Records bereits von der Klasse java.lang.Record erben – und dass sie unveränderlich sein sollen. Das wären sie nicht, wenn sie von einer veränderlichen Klasse erben würden.

Records Sie sind außerdem implizit final, man kann also auch nicht von ihnen erben. Folgender Code ist also ebenfalls ungültig:

public record Point(int x, int y) {}

public class TaggedPoint extends Point {  // Not allowed!
  private String tag;

  TaggedPoint(int x, int y, String tag) {
    super(x, y);
    this.tag = tag;
  }
}Code-Sprache: Java (java)

Besonderheiten von Records

Im Vergleich zu regulären Klassen sollte man bei Records einige Besonderheiten kennen. Diese erkläre ich in den nächsten Abschnitten.

Lokale Records

Records dürfen auch lokal (d. h. innerhalb von Methoden) definiert werden. Das kann insbesondere dann hilfreich sein, wenn man Zwischenergebnisse mit mehreren zusammengehörigen Variablen speichern möchte.

Im folgenden Beispiel definieren wir innerhalb der findFurthestPoint()-Methode den lokalen Record PointWithDistance: eine Kombination aus einem Point und einem double-Wert, der die Entfernung des Punktes zu einem Ursprungspunkt repräsentiert.

Mit Hilfe des lokalen Records füllen wir eine Liste von Punkten und deren Entfernungen zum aktuellen Punkt. Aus dieser Liste ermitteln wir dann denjenigen PointWithDistance mit der größten Distanz – um daraus wiederum den zugehörigen Point zu extrahieren.

public Point findFurthestPoint(Point origin, Point... points) {
  record PointWithDistance(Point point, double distance) {}
  
  List<PointWithDistance> pointsWithDistance = new ArrayList<>();
  for (Point point : points) {
    double distance = origin.distanceTo(point);
    pointsWithDistance.add(new PointWithDistance(point, distance));
  }

  PointWithDistance furthestPointWithDistance = Collections.max(
      pointsWithDistance,
      Comparator.comparing(PointWithDistance::distance));

  return furthestPointWithDistance.point();
}Code-Sprache: Java (java)

Records innerhalb von inneren Klassen

Records dürfen auch innerhalb von inneren Klassen definiert werden:

class OuterClass {
  // ...

  class InnerClass {
    record InnerClassRecord(String foo, int bar) {}

    // ...
  }
}Code-Sprache: Java (java)

Diese Möglichkeit ist insofern erwähnenswert, als dass sie erst mit dem finalen Release von Records durch JDK Enhancement Proposal 395 ermöglicht wurde.

Records und Reflection

Finale Felder von regulären Klassen können problemlos per Reflection verändert werden. Im folgenden Code ist Point die Klasse vom Beginn dieses Artikels.

Point point = new Point(10, 5);
System.out.println("point = " + point);

Field xField = Point.class.getDeclaredField("x");
xField.setAccessible(true);
System.out.println("point.x = " + xField.get(point));

xField.set(point, 55);
System.out.println("point = " + point);Code-Sprache: Java (java)

Der Code gibt folgendes aus:

point = Point[x=10, y=5]
point.x = 10
point = Point[x=55, y=5]Code-Sprache: Klartext (plaintext)

Das bedeutet, wir haben per Reflection das eigentlich private und finale x-Feld der Point-Klasse ausgelesen und geändert!

Wenn wir den gleichen Code mit dem Point-Record aufrufen, dann erhalten wir folgende Ausgabe:

point = Point[x=10, y=5]
point.x = 10
Exception in thread "main" java.lang.IllegalAccessException: 
Can not set final int field eu.happycoders.records.Point.x to java.lang.IntegerCode-Sprache: Klartext (plaintext)

Wir können also auch bei Records per Reflection private Felder auslesen (und damit z. B. einen Accessor umgehen, der eine defensive Kopie einer veränderlichen Komponente, wie z. B. einem Array, erzeugt).

Records sind aber im Gegensatz zu Klassen geschützt vor Veränderungen durch Reflection.

Records deserialisieren

Records haben eine Besonderheit beim Deserialisieren. Diese zeige ich an folgendem Beispiel.

Erweitern wir den Point-Konstruktor zunächst um eine Parameter-Validierung. Sagen wir, wir wollen negative Werte ausschließen. Wir verwenden die kompakte Notation für die Überschreibung des kanonischen Konstruktors. Außerdem machen wir die Klasse serialisierbar:

public record Point(int x, int y) implements Serializable {
  @Serial private static final long serialVersionUID = -1482007299343243215L;

  public Point {
    if (x < 0) throw new IllegalArgumentException("x must be >= 0");
    if (y < 0) throw new IllegalArgumentException("y must be >= 0");
  }
}Code-Sprache: Java (java)

Um den Unterschied beim Deserialisieren zu sehen, erstellen wir eine analoge reguläre Klasse PointClass:

public final class PointClass implements Serializable {
  @Serial private static final long serialVersionUID = 8411630734446201523L;

  private final int x;
  private final int y;

  public Point(int x, int y) {
    if (x < 0) throw new IllegalArgumentException("x must be >= 0");
    if (y < 0) throw new IllegalArgumentException("y must be >= 0");

    this.x = x;
    this.y = y;
  }

  // ... getters, equals(), hashCode(), toString() ...
}Code-Sprache: Java (java)

Wir kommentieren die Parameter-Validierung vorübergehend aus und serialisieren mit folgendem Code einen ungültigen Point-Record und eine ungültige PointClass-Klasse in jeweils eine Datei:

PointClass pc = new PointClass(-5, 5);  // Parameter validation temporarily commented
try (FileOutputStream fileOut = new FileOutputStream("point-class.bin");
    ObjectOutputStream objectOut = new ObjectOutputStream(fileOut)) {
  objectOut.writeObject(pc);
}

Point p = new Point(-5, 5);  // Parameter validation temporarily commented
try (FileOutputStream fileOut = new FileOutputStream("point-record.bin");
    ObjectOutputStream objectOut = new ObjectOutputStream(fileOut)) {
  objectOut.writeObject(p);
}Code-Sprache: Java (java)

Danach kommentieren wir die Parameter-Prüfung wieder ein und versuchen die serialisierten Objekte zu deserialisieren:

try (FileInputStream fileIn = new FileInputStream("point-class.bin");
    ObjectInputStream objectIn = new ObjectInputStream(fileIn)) {
  PointClass pointClass = (PointClass) objectIn.readObject();
  System.out.println("pointClass = " + pointClass);
}

try (FileInputStream fileIn = new FileInputStream("point-record.bin");
    ObjectInputStream objectIn = new ObjectInputStream(fileIn)) {
  Point point = (Point) objectIn.readObject();
  System.out.println("point = " + point);
}Code-Sprache: Java (java)

Das Egebnis macht einen weiteren Unterschied zwischen Records und Klassen sichtbar:

pointClass = PointClass{x=-5, y=5}
Exception in thread "main" java.io.InvalidObjectException: x must be >= 0
	at ...
Caused by: java.lang.IllegalArgumentException: x must be >= 0
	at records.Point.<init>(Point.java:10)Code-Sprache: Klartext (plaintext)

Die fehlerhafte Klasse lässt sich problemlos deserialisieren – der fehlerhafte Record hingegen nicht.

Der Grund: Bei der Deserialisierung einer Klasse werden die aus dem ObjectInputStream gelesenen Werte direkt in die Felder der Klasse geschrieben. Bei einem Record hingegen wird der kanonische Konstruktor aufgerufen – und damit die ggf. darin enthaltenen Parameter-Überprüfungen ausgeführt.

Java Records vs. Klassen

Ich bekomme oft die Frage gestellt, wozu man Records braucht, wenn man doch einfach Konstruktor, Getter, equals(), hashCode() und toString() von einer IDE generieren lassen kann.

Zum einen ist es meiner Meinung nach nicht die Aufgabe einer IDE, die Unzulänglichkeiten einer Programmiersprache auszugleichen. Eine Programmiersprache sollte so ausgereift sein, dass wir mit möglichst wenig Code unsere Ideen ausdrücken können – den Rest sollte der Compiler erledigen. Es verwenden auch nicht alle die gleiche IDE, und unterschiedliche IDEs generieren unterschiedlichen Quellcode.

Records haben weitere, konkrete Vorteile:

  • Die Felder von Records sind wirklich unveränderlich. Während bei einer regulären Klasse finale Felder per Reflection verändert werden können, ist dies bei Records nicht möglich (s. Abschnitt Records und Reflection).
  • Es ist nicht möglich per Deserialisierung ungültige Records zu erzeugen, da bei der Deserialisierung (im Gegensatz zu Klassen) der kanonische Konstruktor des Records aufgerufen wird.
  • Für die Methoden equals(), hashCode() und toString() erzeugt der Compiler einen speziellen Bytecode, der spezielle Implementierungen dieser Methoden in der JVM aufruft. So können diese Methoden in zukünftigen Java-Versionen immer weiter optimiert werden, ohne dass existierender Code neu kompiliert werden muss.
  • Records arbeiten eng mit anderen Sprachmerkmalen zusammen, wie z. B. den in Java 21 finalisierten Records Patterns.

Java Records vs. Lombok

Ähnlich oft bekomme ich die Frage gestellt, was der Vorteil von Records gegenüber Lombok ist.

Meiner Meinung nach sollten wir nicht die Verantwortung für etwas, das eine Sprache können sollte, an eine Library abgeben. Denn das birgt Risiken: Was, wenn die Library nicht weiter gewartet wird? Was, wenn sie nicht an neue Java-Versionen angepasst oder gar komplett eingestellt wird?

Zudem gelten die gleichen konkreten Nachteilte wie bei regulären Klassen: Auch finale Felder von Klassen können per Reflection verändert werden; durch Deserialisierung können ungültige Instanzen erzeugt werden; equals(), hashCode() und toString() können nicht durch die JVM optimiert werden; und auch Pattern Matching funktioniert nicht mit Lombok-annotierten Klassen.

Fazit

Records bieten eine kompakte Notationsmöglichkeit, um Java-Klassen mit ausschließlich finalen Feldern zu definieren. Records enthalten automatisch einen Konstruktor, der alle finalen Felder setzt (den kanonischen Konstruktor), lesende Zugriffsmethoden für alle Felder (die Accessor-Methoden), sowie durch die JVM optimierte equals()-, hashCode()- und toString()-Methoden.

Records können um weitere Konstruktoren, statische Felder und statische sowie nicht-statische Methoden erweitert werden. Der kanonische Konstruktor kann überschrieben werden.

Records können Interfaces implementieren (auch versiegelte), aber keine Klassen erweitern, und von ihnen kann auch nicht geerbt werden.

Beim Deserialisieren von Records wird immer deren kanonischer Konstruktor – und die darin ggf. enthaltenen Parameter-Validierungen – aufgerufen.

Records wurden in Java 14 und Java 15 als Preview-Features und durch JDK Enhancement Proposal 395 in Java 16 als produktionsreif eingestuft. Records wurden im Rahmen von Projekt Amber entwickelt, innerhalb dessen auch Switch Expressions, Text Blocks und Pattern Matching und Sealed Classes entwickelt wurden.

Wenn dir der Artikel gefallen hat, teile ihn gerne über einen der Share-Buttons am Ende oder hinterlasse einen Kommentar. Wenn du informiert werden möchtest, wenn der nächste Artikel auf HappyCoders.eu veröffentlicht wird, klicke hier, um dich für den HappyCoders-Newsletter anzumelden.