None

Java Records – eine Einführung

Records bilden einen Baustein für modernes, expressives Java.Zamrznuti tonovi | shutterstock.com



Mit Records wurde Java fundamental in Richtung eines datenzentrierten Stils weiterentwickelt. Dabei handelt es sich um eine neuere Form von Java-Klassen, um Daten zu speichern. Diese wurde mit Java 14 als Preview eingeführt und mit Java 16 zum Standard. Ihr Sinn und Zweck in aller Kürze: Anstatt Boilerplate-Code für Konstruktoren, Accessoren, equals(), hashCode() und toString() zu schreiben, deklarieren Sie einfach die Felder und überlassen dem Java-Compiler den Rest.



In diesem Artikel vermitteln wir Ihnen die wichtigsten Grundlagen zu Java Records – verdeutlicht an einigen grundlegenden und fortgeschrittenen Use Cases. Darüber hinaus erfahren Sie auch, für welche Anwendungsszenarien diese Datenklassen nicht geeignet sind.  



Record-Klassen und der Java-Compiler



Einfache Datenklassen zu erstellen, erforderte in Java traditionell jede Menge Boilerplate Code. Verdeutlicht wird das durch einen Blick darauf, wie wir die Java-Maskottchen Duke und Juggy darstellen würden:




public class JavaMascot {
private final String name;
private final int yearCreated;

public JavaMascot(String name, int yearCreated) {
this.name = name;
this.yearCreated = yearCreated;
}

public String getName() { return name; }
public int getYearCreated() { return yearCreated; }

// equals, hashCode and toString methods omitted for brevity
}




Mit Records lässt sich dieser Code auf eine einzige Zeile reduzieren:




public record JavaMascot(String name, int yearCreated) {}




Diese übersichtliche Deklaration stellt automatisch private Felder, einen Konstruktor, Accessor-Methoden sowie ordnungsgemäß implementierte equals()-, hashCode()– und toString()-Methoden bereit.



Nachdem wir nun den JavaMascot-Record definiert haben, können wir ihn einsetzen:



public class RecordExample {
public static void main(String[] args) {
JavaMascot duke = new JavaMascot("Duke", 1996);
JavaMascot juggy1 = new JavaMascot("Juggy", 2005);
JavaMascot juggy2 = new JavaMascot("Juggy", 2005);

System.out.println(duke); // JavaMascot[name=Duke, yearCreated=1996]
System.out.println(juggy1.equals(juggy2)); // true
System.out.println(duke.equals(juggy1)); // false
System.out.println("Mascot name: " + duke.name());
System.out.println("Created in: " + duke.yearCreated());
}
}



Records bieten automatisch eine aussagekräftige String-Darstellung, eine wertebasierte Gleichheitsprüfung und simplere Accessor-Methoden, die mit den Komponentennamen übereinstimmen.



Records anpassen



Obwohl Records von Natur aus prägnant sind, lassen sie sich dennoch mit benutzerdefinierten Funktionen erweitern – wie die folgenden Beispiele demonstrieren.



Kompakte Konstruktoren



Records stellen eine spezielle “Compact Constructor”-Syntax bereit, mit der sich Input-Parameter validieren oder transformieren lassen, ohne die Parameterliste zu wiederholen:




record JavaMascot(String name, int yearCreated) {
// Compact constructor with validation
public JavaMascot {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be empty");
}
if (yearCreated < 1995) {
throw new IllegalArgumentException("Java mascots didn't exist before 1995");
}
}
}



Der Compact Constructor wird nach der Initialisierung der Felder ausgeführt, aber bevor das Objekt vollständig erstellt ist. Er eignet sich daher ideal für die Validierung. In diesem Beispiel werden die Parameter-Deklarationen weggelassen. Sie sind jedoch implizit im Konstruktor verfügbar.



Methoden hinzufügen



Records lassen sich außerdem auch um Methoden erweitern:



record JavaMascot(String name, int yearCreated) {
public boolean isOriginalMascot() {
return name.equals("Duke");
}

public int yearsActive() {
return java.time.Year.now().getValue() - yearCreated;
}
}



Durch Methoden können Records Verhalten bezüglich ihrer Daten kapseln, während die Syntax übersichtzlich und die Daten unveränderlich bleiben.



Records meet Pattern Matching



Records wurden mit Java 21 zu einem wichtigen Pattern-Matching-Aspekt, mit dem Support von Switch Expressions, der Destrukturierung von Komponenten sowie verschachtelter Muster und Guard Conditions. In Kombination mit dem erweiterten instanceof-Operator ermöglichen Records, Komponenten während der Typprüfung kompakt zu extrahieren:



record Person(String name, int age) {}

if (obj instanceof Person person) {
System.out.println("Name: " + person.name());
}



Ein traditionelleres Beispiel: Geometrische Formen sind eine klassische Methode, um zu veranschaulichen, wie Sealed Interfaces mit Records funktionieren – und sie veranschaulichen Pattern Matching in besonderem Maße. Die Eleganz dieser Kombination zeigt sich in Switch-Ausdrücken (eingeführt in Java 17), mit denen Sie prägnanten, typsicheren Code schreiben können, der an algebraische Datentypen in funktionalen Sprachen erinnert:



sealed interface Shape permits Rectangle, Circle, Triangle {}

record Rectangle(double width, double height) implements Shape {}
record Circle(double radius) implements Shape {}
record Triangle(double base, double height) implements Shape {}

public class RecordPatternMatchingExample {
public static void main(String[] args) {
Shape shape = new Circle(5);

// Expressive, type-safe pattern matching
double area = switch (shape) {
case Rectangle r -> r.width() * r.height();
case Circle c -> Math.PI * c.radius() * c.radius();
case Triangle t -> t.base() * t.height() / 2;
};

System.out.println("Area = " + area);
}
}



Hier ist der Shape-Typ ein Sealed Interface, das lediglich Rectangle, Circle und Triangle zulässt. Weil dieses Set in sich geschlossen ist, ist der Switch “exhaustive” und erfordert keinen Default-Branch.



Records als Data Transfer Objects



In modernen API-Designs wie REST, GraphQL, gRPC oder der Kommunikation zwischen Services sind Java Records außerordentlich gut als Data Transfer Objects (DTOs) geeignet. Ihre prägnante Syntax und integrierte Equality sind ideal, um zwischen Service-Ebenen zu “mappen”. Ein Beispiel:




record UserDTO(String username, String email, Set roles) {}
record OrderDTO(UUID id, UserDTO user, List items, BigDecimal total) {}



In Microservices-Anwendungen sind DTOs allgegenwärtig. Eine Java Record zu verwenden, gestaltet diese robuster und übersichtlicher.



Java Records – funktional und parallel



Records ergänzen funktionale und parallele Programmierung als unveränderliche Datencontainer. Sie eignen sich gut als Rückgabetypen von reinen Funktionen, innerhalb von Stream-Pipelines und, um Daten über Threads hinweg sicher gemeinsam zu nutzen.



Weil Felder in Records endgültig und unveränderlich sind, wird eine ganze Reihe von Threading-Problemen vermieden. Einmal erstellt, kann sich ihr Status nicht mehr ändern – Records sind also thread-safe ohne defensives Copying oder Synchronisation. Ein weiteres Beispiel:



transactions.parallelStream().mapToDouble(Transaction::amount).sum();



Weil Records unveränderlich sind, ist diese parallele Berechnung von Natur aus thread-safe.



Wofür Java Records nicht geeignet sind



Java Records haben wie gesehen ihre Stärken. Aber sie sind kein Universalersatz. Beispielsweise erweitert jeder Datensatz implizit java.lang.Record. Deswegen können Records keine anderen Klassen erweitern (obwohl sie Schnittstellen implementieren können). Records eignen sich nicht für Szenarien, in denen Klassenvererbung erforderlich ist. Nachfolgend einige weitere Situationen, für die Java Records nicht zu empfehlen sind.



Mutable/Stateful-Erfordernisse



Record-Komponenten sind immer endgültig. Sie passen also nicht in Programme, die mutable- oder stateful-Objekte erfordern. Das folgende Beispiel zeigt eine veränderliche Klasse, die auf einer Zustandsänderung basiert – was bei Records nicht möglich ist:



public class GameCharacter {
private int health;
private Position position;

public void takeDamage(int amount) {
this.health = Math.max(0, this.health - amount);
}

public void move(int x, int y) {
this.position = new Position(this.position.x() + x, this.position.y() + y);
}
}



Komplexe Logiken



Designs, die auf veränderbaren Zuständen, einer komplexen Geschäftslogik oder Mustern wie Strategy, Visitor oder Observer fußen, lassen sich mit traditionellen Klassen besser umsetzen. Hier ein Beispiel für komplexe Logik, die für Records nicht geeignet ist:



public class TaxCalculator {
private final TaxRateProvider rateProvider;
private final DeductionRegistry deductions;

public TaxAssessment calculateTax(Income income, Residence residence) {
// Complex logic that doesn’t suit a record
}
}



Inkompatible Frameworks



Einige Frameworks, insbesondere ORMs, vertragen sich möglicherweise nicht gut mit Java Records. Auch Tools, die stark auf Serialisierung oder Reflexion setzen, können Probleme verursachen. Generell ist zu empfehlen, Java-Funktionen auf Kompatibilität mit Ihrem Tech-Stack zu prüfen:




// May not work well with some ORM frameworks
record Employee(Long id, String name, Department department) {}

// Instead, you might need a traditional entity class
@Entity
public class Employee {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
private Department department;

// Getters, setters, equals, hashCode, etc.
}



Java Records und Serialisierung



Records sind im gesamten Java-Ökosystem inzwischen weit verbreitet. Ihre Unveränderlichkeit macht sie in Sachen Persistenz, Konfiguration und Datenübertragung attraktiv. Wie jede andere Klasse kann Records das Serializable-Interface implementieren. Serialisierbare Record-Komponenten eignen sich ideal für Anwendungsfälle wie:




Konfigurationen speichern,



Zustände wiederherstellen,



Daten über das Netzwerk zu senden, oder



Werte zwischenspeichern.




Die endgültigen und unveränderlichen Record-Felder unterstützen dabei, Probleme zu vermeiden. Diese können auftreten, wenn sich der Mutable State zwischen Serialisierung und Deserialisierung verändert:



import java.io.Serializable;

record User(String username, int age, Profile profile) implements Serializable {}

class Profile {
private String bio;
}



In diesem Beispiel ist Profile nicht serialisierbar – und damit auch User. Wenn Sie Profile so aktualisieren, dass es auch Serializable implementiert, ist auch User vollständig serialisierbar:



class Profile implements Serializable {
private String bio;
}



Über die Grundlagen der Serialisierung hinaus hat sich der Ökosystem-Support für Java Records rasant weiterentwickelt. Beliebte Frameworks wie Spring Boot, Quarkus und Jackson arbeiten – ebenso wie die meisten Testing-Tools – nahtlos mit den neueren Java-Datenklassen zusammen. So eignen sich Records auch hervorragend als DTOs in realen APIs:



@RestController
@RequestMapping("/api/orders")
public class OrderController {

@GetMapping("/{id}")
public OrderView getOrder(@PathVariable UUID id) {
// In a real app, this would come from a database or service
return new OrderView(
id,
"Duke",
List.of(new ItemView(UUID.randomUUID(), 2)),
new BigDecimal("149.99")
);
}

// Record DTOs for API response
record OrderView(UUID id, String customerName, List items, BigDecimal total) {}
record ItemView(UUID productId, int quantity) {}
}



(fm)