1. Record-Klassen und Unveränderlichkeit
Unveränderlichkeit ist eine Eigenschaft eines Objekts, bei der sein Zustand nach der Erstellung nicht mehr geändert werden kann. Anders ausgedrückt: Ist das Objekt einmal erstellt, können Sie seine internen Daten nicht mehr ändern. Punkt! Wie in Stein gemeißelt.
Stellen Sie sich ein Zugticket vor. Sobald es gedruckt ist, können Sie Datum oder Abfahrtsort nicht mehr ändern (es sei denn, man rechnet mit pfiffigen Photoshop-Spielereien). Ein Ticket ist ein unveränderliches Objekt. Wenn Sie ein anderes Ticket wollen – kaufen Sie ein neues.
In der Programmierung nennt man solche Objekte unveränderliche Objekte. Sie schützen das Programm vor unbeabsichtigten Änderungen und machen den Code sicherer und vorhersehbarer.
Merkmale eines unveränderlichen Objekts
- Alle Felder des Objekts sind final (sie können nur einmal zugewiesen werden, gewöhnlich im Konstruktor).
- Es gibt keine Setter (Methoden, die Feldwerte ändern).
- Alle Methoden, die interne Daten zurückgeben, liefern entweder Kopien zurück oder die Daten selbst sind ebenfalls unveränderlich.
Record-Klassen in Java wurden genau dafür erfunden, unveränderliche Datenstrukturen einfach und ohne Schmerzen zu erstellen.
Warum ist ein Record unveränderlich?
- Alle Komponenten eines Records sind final.
Wenn Sie einen record deklarieren, macht der Compiler automatisch alle seine Felder private und final. Das bedeutet: Nach der Erstellung eines Objekts können Sie seine Felder nicht mehr ändern. - Keine Setter.
In einer Record-Klasse können Sie keine Methode setX(int x) hinzufügen – der Compiler lässt es nicht zu, den Feldwert nach der Objekterstellung zu ändern. - Der Konstruktor weist Werte nur einmal zu.
Alle Werte werden ausschließlich beim Erzeugen des Objekts gesetzt.
Beispiel
public record Point(int x, int y) {}
Point p = new Point(5, 10);
// p.x = 7; // Kompilierungsfehler: Feld x ist private und final
// p.x(7); // Fehler: kein Setter vorhanden!
System.out.println(p.x()); // 5
Der Versuch, ein Feld zu ändern oder einen nicht vorhandenen Setter aufzurufen, führt zu einem Kompilierungsfehler. Java achtet streng auf den Unveränderlichkeitsvertrag.
2. Vorteile unveränderlicher Objekte
Sicherheit in der Nebenläufigkeit
In nebenläufigen Programmen (und das sind heute die meisten!) sind unveränderliche Objekte wie eine schusssichere Weste. Wenn ein Objekt nicht geändert werden kann, können verschiedene Threads es gefahrlos lesen, ohne befürchten zu müssen, dass jemand in diesem Moment die Daten verändert. Man muss den Zugriff nicht synchronisieren und muss sich nicht um Datenrennen sorgen.
Fakt: Viele Klassen in der Standardbibliothek von Java, die in nebenläufigen Szenarien intensiv genutzt werden, sind entweder unveränderlich oder speziell gegen Änderungen geschützt.
Leichtere Verständlichkeit des Codes
Wenn ein Objekt unveränderlich ist, können Sie sicher sein: Sie geben es an eine andere Methode oder Klasse weiter – und es ändert sich nicht „hinter Ihrem Rücken“. Das erleichtert das Lesen und Debuggen des Codes erheblich. Man muss nicht rätseln, wer wo ein Feld geändert haben könnte – niemand konnte es ändern!
Gut als Schlüssel in Collections geeignet
Unveränderliche Objekte eignen sich hervorragend als Schlüssel in Collections wie HashMap oder HashSet. Warum? Weil ihre equals- und hashCode-Methoden nur von Feldern abhängen, die sich nicht ändern. Das heißt, ein Objekt „geht“ in der Collection nicht „verloren“, weil sich sein Zustand geändert hat.
Weniger versteckte Fehler
Ein veränderliches Objekt lässt sich leicht verderben, wenn man eine Referenz versehentlich an die falsche Stelle weitergibt. Ein unveränderliches Objekt hingegen ist wie ein gedrucktes Buch: Niemand kann eine Seite herausreißen oder überschreiben.
3. Vergleich mit normalen Klassen
Vergleichen wir das Verhalten einer normalen Klasse mit dem einer Record-Klasse. Als Beispiel nehmen wir ein einfaches Modell eines Punktes in der Ebene.
Normale Klasse (mutable)
public class PointClass {
private int x;
private int y;
public PointClass(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
}
Man kann ein Objekt erstellen und seinen Zustand anschließend nach Belieben ändern:
PointClass p = new PointClass(1, 2);
p.setX(10); // p.x ist jetzt 10
Record-Klasse (immutable)
public record Point(int x, int y) {}
Sie erstellen ein Objekt – und das war’s, es bleibt für immer so, wie es erstellt wurde:
Point p = new Point(1, 2);
// p.x = 10; // Fehler! Kein Zugriff auf das Feld
// p.x(10); // Fehler! Kein Setter
Tabelle: Verhaltensvergleich
| Normale Klasse | Record-Klasse | |
|---|---|---|
| Felder | beliebig | nur private final |
| Setter | können hinzugefügt werden | können nicht hinzugefügt werden |
| Veränderlichkeit | veränderlich (mutable) | unveränderlich |
| Automatische Generierung | nein | ja (equals, hashCode, toString) |
4. Praxis: Unveränderlichkeit von Record-Klassen nutzen
Probieren wir aus, wie man eine Record-Klasse in einem kleinen Beispiel verwendet. Angenommen, wir haben eine Banking-Anwendung und möchten Informationen über eine Transaktion speichern:
public record Transaction(String fromAccount, String toAccount, double amount) {}
Objekt erzeugen:
Transaction t = new Transaction("12345", "67890", 1500.0);
System.out.println(t);
// Transaction[fromAccount=12345, toAccount=67890, amount=1500.0]
Versuchen wir, Geld auf ein anderes Konto zu „überweisen“:
// t.toAccount = "11111"; // Fehler! Feld ist final, kein Zugriff
// t.toAccount("11111"); // Fehler! Kein Setter
Wenn wir eine andere Transaktion brauchen – erzeugen wir ein neues Objekt:
Transaction t2 = new Transaction(t.fromAccount(), "11111", t.amount());
Wichtig: Unveränderlichkeit heißt nicht „unpraktisch“. Es ist nur ein anderer Arbeitsstil: Wenn Sie einen neuen Zustand brauchen – erzeugen Sie ein neues Objekt.
5. Besonderheiten der Unveränderlichkeit: Was man beachten sollte
Unveränderlichkeit ist nicht immer absolut!
Eine Record-Klasse garantiert, dass ihre Felder sich nicht ändern. Wenn ein Feld jedoch eine Referenz auf ein veränderliches Objekt ist (z. B. ein Array oder eine normale Klasse), kann der Inhalt dieses Objekts sehr wohl verändert werden.
Beispiel mit einem Array
public record DataHolder(int[] data) {}
int[] arr = {1, 2, 3};
DataHolder holder = new DataHolder(arr);
arr[0] = 99;
System.out.println(holder.data()[0]); // 99! Das Array wurde verändert
Schlussfolgerung: Wenn Sie echte Unveränderlichkeit wollen, verwenden Sie nur unveränderliche Typen (String, Integer, andere records usw.) oder erstellen Sie Kopien veränderlicher Objekte im kanonischen Konstruktor der Record-Klasse. Zum Beispiel:
int[] copy = Arrays.copyOf(data, data.length);
6. Wie man eine normale Klasse unveränderlich macht
Wenn Sie eine normale Klasse immutable (unveränderlich) machen möchten, müssen Sie es manuell umsetzen:
- Alle Felder als private final markieren,
- Keine Setter hinzufügen,
- Alle Felder nur im Konstruktor initialisieren,
- Wenn ein Feld ein veränderliches Objekt ist, eine defensive Kopie erstellen (defensive copy).
Beispiel
public final class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String name() {
return name;
}
public int age() {
return age;
}
}
Zugegeben, mit einer Record-Klasse geht das einfacher und kürzer:
public record User(String name, int age) {}
7. Typische Fehler im Umgang mit unveränderlichen Record-Klassen
Fehler Nr. 1: Versuch, ein Feld nach der Erstellung zu ändern.
Anfänger versuchen oft, für ein Record-Objekt p.x = 42; oder p.x(42); zu schreiben. Aber der Compiler sagt sofort: „So geht das nicht! Feld final, kein Setter vorhanden.“
Fehler Nr. 2: Verwendung veränderlicher Objekte als Record-Komponenten.
Wenn Sie einem record ein Feld vom Typ List, Map, ein Array oder ein anderes veränderliches Objekt hinzufügen, schützt der Record Sie nicht vor Änderungen am Inhalt dieses Objekts. Wenn Sie zum Beispiel einen record User(List<String> hobbies) haben, kann jemand ein Element zur Liste hinzufügen oder daraus entfernen – und damit den Zustand Ihres Record-Objekts ändern. Um das zu vermeiden, verwenden Sie unveränderliche Collections (List.copyOf, Collections.unmodifiableList) oder erstellen Sie Kopien der Collections im Konstruktor des Records.
Fehler Nr. 3: Falsches Verständnis von Unveränderlichkeit.
Manche glauben, dass ein record vor jeglichen Änderungen geschützt ist. In Wirklichkeit kann der Inhalt veränderlicher Objekte, auf die die Felder verweisen, geändert werden – was zu unerwarteten Fehlern führt.
GO TO FULL VERSION