1. Vergleich von record und class: Worin liegen die wichtigsten Unterschiede?
In Java gibt es zwei Hauptmöglichkeiten, eigene Datentypen zu beschreiben: mit normalen Klassen (class) und mit Record-Klassen (record). Auf den ersten Blick erlauben beide Varianten, Daten zu speichern und zu verarbeiten. Schaut man jedoch genauer hin, gibt es mehr Unterschiede, als es zunächst scheint!
Vergleichstabelle: class vs record
| Merkmal | Normale Klasse (class) | Record-Klasse (record) |
|---|---|---|
| Mutabilität | Beliebig: Felder können final sein oder nicht | Unveränderlich: alle Felder sind final |
| Vererbung | Kann erben (extends), standardmäßig nicht final | Immer final, kann keine Superklasse sein |
| Felder | Beliebig: statisch, nicht statisch, final oder nicht-final, beliebige Typen | Nur Komponenten des record (private final), plus statische Felder |
| Getter/Setter | Schreiben wir selbst (oder generieren sie mit Lombok) | Getter werden automatisch erzeugt (Feldname = Methodenname), keine Setter |
| equals/hashCode/toString | Üblicherweise manuell schreiben/generieren (equals, hashCode, toString) | Werden automatisch über alle Komponenten generiert |
| Konstruktoren | Beliebig viele | Ein primärer (für alle Komponenten), ein kompakter Konstruktor kann ergänzt werden |
| Interfaces | Können implementiert werden | Können implementiert werden |
| Zusätzliche Methoden | Beliebige | Dürfen hinzugefügt werden, aber nur Methoden (keine Felder) |
| Verwendung in Collections | Möglich, aber equals/hashCode müssen korrekt implementiert werden | Ideal als Schlüssel/Werte geeignet, alles ist bereits implementiert |
Beispiel zur Veranschaulichung
Normale Klasse:
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
Record-Klasse:
public record Person(String name, int age) { }
Das war’s! Eine einzige Codezeile – und wir erhalten dasselbe (und sogar mehr). Und kein Risiko, etwas Wichtiges bei der Implementierung zu vergessen.
2. Einschränkungen von Record-Klassen
Record-Klassen sind nicht nur „kurze Syntax“, sondern ein eigenes Konzept mit klaren Regeln. Schauen wir sie uns genauer an.
Record ist immer final
Eine Record-Klasse ist per Definition immer final. Das bedeutet, Sie können keine Unterklasse von record erstellen:
public record Point(int x, int y) { }
// public class ColoredPoint extends Point { } // Kompilierungsfehler!
Wenn Sie das Verhalten erweitern müssen, verwenden Sie normale Klassen oder Komposition (betten Sie den record in eine Klasse ein).
Record kann keine Superklasse sein
Eine Record-Klasse kann keine Elternklasse für andere Klassen sein, sie ist immer final. Das ist logisch: Wäre das möglich, könnte jemand ein veränderliches Feld hinzufügen – und das gesamte Konzept „unveränderlicher Daten“ würde zusammenbrechen.
Nur final-Felder (Komponenten)
Alle Komponenten eines record werden im Header deklariert und sind standardmäßig private final. Sie können dem Rumpf eines record keine nichtstatischen Felder hinzufügen:
public record User(String login, String email) {
// int counter; // Fehler! Nichtstatische Felder dürfen nicht hinzugefügt werden
static int totalUsers = 0; // Zulässig, das ist ein statisches Feld
}
Keine Setter
Eine Record-Klasse kann keine Setter für die Komponenten haben. Jeder Versuch, eine Methode wie setX(int x) hinzuzufügen, ist sinnlos: Sie können den Feldwert nach der Objekterzeugung nicht ändern.
public record Point(int x, int y) {
// public void setX(int x) { this.x = x; } // Fehler: final-Feld kann nicht geändert werden
}
Kein parameterloser Konstruktor
Eine record-Klasse hat stets nur den primären Konstruktor, der Werte für alle Komponenten entgegennimmt. Ein record kann nicht ohne Angabe aller Daten erstellt werden:
Point p = new Point(1, 2); // OK
// Point p = new Point(); // Fehler: kein parameterloser Konstruktor vorhanden
Keine nichtstatischen Initialisierer
Eine Record-Klasse darf keine nichtstatischen Initialisierer enthalten (die, die in geschweiften Klammern außerhalb von Methoden stehen):
public record User(String login) {
// { /* ... */ } // Fehler: nichtstatische Initialisierer sind verboten
}
Einschränkungen bei der Vererbung
Eine Record-Klasse kann keine andere Klasse explizit erweitern (außer java.lang.Record, das als Basisklasse für alle records verborgen ist). Interfaces implementieren – gern!
public interface Printable {
void print();
}
public record Book(String title) implements Printable {
@Override
public void print() {
System.out.println("Wir drucken das Buch: " + title);
}
}
Nicht geeignet für komplexe Businesslogik
record steht für Daten, nicht für Verhalten. Wenn Ihr Objekt komplexe Logik, veränderlichen Zustand, einen „Lebenszyklus“ oder viele Abhängigkeiten hat – hilft record hier nicht. Verwenden Sie besser eine normale Klasse.
3. Wann sollte man Record-Klassen verwenden?
- DTO (Data Transfer Object): zum Übertragen unveränderlicher Daten zwischen Anwendungsschichten, Services, Microservices oder REST-Controllern (z. B. in JSON-Antworten).
- Value Object: Objekte, die ausschließlich durch ihre Werte definiert sind.
- Schlüssel und Werte in Collections: wenn eine korrekte Implementierung von equals und hashCode wichtig ist (z. B. für die Verwendung in HashMap oder Set).
- Berechnungsergebnisse: wenn eine Methode mehrere Werte auf einmal zurückgeben soll (z. B. record Pair<T, U>(T first, U second)).
Beispiel: DTO für einen REST-Controller
public record UserDto(String login, String email) { }
Nun kann man aus dem Controller gefahrlos ein Objekt dieses Typs zurückgeben, ohne Sorge, dass jemand seine Felder verändert.
Beispiel: Schlüssel für HashMap
public record Point(int x, int y) { }
Map<Point, String> pointNames = new HashMap<>();
pointNames.put(new Point(1, 2), "A");
pointNames.put(new Point(3, 4), "B");
// Alles funktioniert korrekt: equals und hashCode sind bereits implementiert!
4. Wann man Record-Klassen NICHT verwenden sollte
- Veränderlicher Zustand: wenn sich mindestens ein Feld nach der Objekterzeugung ändern können muss.
- Komplexe Logik: wenn das Objekt komplexes Verhalten, viele Methoden und verschachtelte Objekte mit veränderlichem Zustand hat.
- Vererbung: wenn eine Klassenhierarchie, abstrakte Basisklassen und Methodenüberschreibungen erforderlich sind.
- Domänen-/Geschäftsentitäten: zum Beispiel Objekte, die in einer Datenbank leben und eine eindeutige Kennung besitzen.
Beispiel: wenn eine normale Klasse nötig ist
public class Account {
private String id;
private int balance;
public Account(String id, int balance) {
this.id = id;
this.balance = balance;
}
public void deposit(int amount) { balance += amount; }
public void withdraw(int amount) { balance -= amount; }
// getters, setters, equals, hashCode, toString...
}
Hier ist klar zu sehen, dass sich der Objektzustand ändert – record ist ungeeignet.
5. Praxisbeispiele: Wahl zwischen record und class
Beispiel 1: record – die ideale Wahl
public record Rectangle(int width, int height) {
public int area() {
return width * height;
}
}
- Das Rechteck wird ausschließlich durch Breite und Höhe definiert.
- Diese Werte müssen nach der Erstellung nicht geändert werden.
- Eine nützliche Methode area() kann hinzugefügt werden.
- Den Rest erledigt Java für Sie.
Beispiel 2: class – die bessere Wahl
public class MutableRectangle {
private int width;
private int height;
public MutableRectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int area() { return width * height; }
}
Müssen die Abmessungen des Rechtecks nach der Erstellung geändert werden? Verwenden Sie eine normale Klasse.
6. Typische Fehler im Umgang mit Record-Klassen
Fehler Nr. 1: Versuch, ein nichtstatisches Feld hinzuzufügen.
Eine Record-Klasse erlaubt es nicht, nichtstatische Felder außerhalb der Komponentenliste zu deklarieren. Wird es versucht – meldet der Compiler einen Fehler. Zum Beispiel:
public record City(String name) {
// int population; // Fehler!
}
Fehler Nr. 2: Wunsch, einen Setter hinzuzufügen.
Record unterstützt keine Setter für Komponenten. Jeder Versuch, den Wert eines Feldes nach der Objekterstellung zu ändern – ein Kompilierungsfehler.
Fehler Nr. 3: Versuch, von record zu erben oder einen record zu erben.
Record ist immer final. Von einem record kann nicht geerbt werden, und ein record kann keine andere Klasse erweitern (außer dem verborgenen java.lang.Record).
Fehler Nr. 4: Verwendung von record für veränderliche Objekte.
Wenn Sie planen, den Zustand eines Objekts nach seiner Erstellung zu ändern – ist record nichts für Sie! Verwenden Sie eine normale Klasse.
Fehler Nr. 5: Einschränkungen des Konstruktors übersehen.
Eine Record-Klasse muss einen Konstruktor besitzen, der Werte für alle Komponenten entgegennimmt. Es gibt keinen parameterlosen Konstruktor!
GO TO FULL VERSION