Was ist ein „unveränderliches Objekt“ und wofür wird es benötigt?
Unveränderliches Objekt ist ein Objekt, dessen Zustand nach der Konstruktion nicht verändert werden kann. Vorteile:
- Standardmäßig thread-sicher (ohne Synchronisierung).
- Einfache Invarianz: Wenn ein Objekt im Konstruktor gültig ist, ist es immer gültig.
- Sichere gemeinsame Nutzung/Caching (kann frei zwischen Threads übertragen und in Caches gelegt werden).
- Praktisch als Schlüssel in
Map/Elemente inSet(konsistenterhashCode). - Einfacher zu testen und zu verstehen.
Regeln für die Erstellung eines unveränderlichen Klassens
- Mache die Klasse
final(oder alle Felderprivate final+ Vererbung logisch verbieten). - Alle Felder sind
private final. - Initialisierung nur im Konstruktor (keine Setter danach).
- Vermeide
this-Referenzen aus dem Konstruktor (keine Übergabe eines Links auf sich selbst, bevor die Initialisierung abgeschlossen ist). - Defensive Kopie für:
- Eingangswerte: veränderliche Argumente (Arrays, Collections,
Date, alle mutable Typen); - Rückgabewerte: Getter sollten keine “lebenden” internen Referenzen zurückgeben.
- Tiefe Unveränderlichkeit: Wenn ein Feld eine Collection/Array/Objekt ist, sorge für eine tiefe Schutzmaßnahmen (siehe unten).
Beispiel (korrekt)
import java.util.List;
import java.util.Objects;
public final class Money {
private final String currency; // unveränderlicher Typ (String ist unveränderlich)
private final long minorUnits; // Pfennige/Cent
private final List<Integer> tags; // BEISPIEL: Collection ist von Natur aus veränderlich
public Money(String currency, long minorUnits, List<Integer> tags) {
// defensive copy + unveränderliche Hülle
this.currency = Objects.requireNonNull(currency);
this.minorUnits = minorUnits;
this.tags = List.copyOf(tags); // Java 10+: im Grunde ein unmodifizierbarer Snapshot
}
public String currency() { return currency; }
public long minorUnits() { return minorUnits; }
public List<Integer> tags() {
// kann tags direkt zurückgeben, da copyOf einen unveränderlichen Snapshot erstellt
return tags;
}
// funktionaler Stil "mit Änderung": erstelle ein NEUES Objekt mit einem anderen Wert
public Money withMinorUnits(long newMinorUnits) {
return new Money(this.currency, newMinorUnits, this.tags);
}
@Override public boolean equals(Object o) { /* standardmäßig anhand der zwei Felder + tags */ }
@Override public int hashCode() { /* konsistent mit equals */ }
@Override public String toString() { /* nützlich für Logs */ }
}Erläuterungen:
List.copyOf(...)gibt einen unveränderlichen Snapshot des aktuellen Inhalts zurück (und wirftNullPointerExceptionbeinull-Elementen).- Wenn eine tiefe Unveränderlichkeit benötigt wird (Elemente der Collection sind selbst veränderlich), kopiere die Elemente (siehe „Tiefe Unveränderlichkeit“).
- Für oberflächliche Unveränderlichkeit:
List.copyOf,Set.copyOf,Map.copyOf(Java 10+) oderCollections.unmodifiableXxx(...). unmodifiableListverhindert nur die Mutation über den Wrapper, aber wenn du eine Referenz auf die ursprüngliche veränderliche Collection hältst, sind Änderungen sichtbar. Daher:- Entweder erstelle eine neue Collection und hülle sie ein,
- oder verwende
copyOf(erstellt einen Snapshot).
Umgang mit Arrays und Collections
Arrays
Arrays sind veränderlich. Das bedeutet:
- Im Konstruktor:
this.arr = Arrays.copyOf(arr, arr.length); - Im Getter:
return Arrays.copyOf(arr, arr.length);
Collections
- Für oberflächliche Unveränderlichkeit:
List.copyOf,Set.copyOf,Map.copyOf(Java 10+) oderCollections.unmodifiableXxx(...). unmodifiableListverhindert nur die Mutation über den Wrapper, aber wenn du eine Referenz auf die ursprüngliche veränderliche Collection hältst, sind Änderungen sichtbar. Daher:- Entweder erstelle eine neue Collection und hülle sie ein,
- oder verwende
copyOf(erstellt einen Snapshot).
- Für tiefe Unveränderlichkeit: Kopiere die Elemente (wenn sie veränderlich sind).
Häufige Fehler (die Interviews lieben)
- Rückgabe einer lebenden Referenz auf eine interne Collection/Array:
Ein „unveränderlicher“ Klasse, der
getList()zurückgibt und diese dann verändert werden kann, ist nicht unveränderlich. - Keine defensive Kopie im Konstruktor: Du hast eine Liste übergeben, die Referenz gespeichert und nicht kopiert → die Klasse ist „kaputt“, wenn die Liste nach dem Konstruktor verändert wird.
- Veränderliche Typen innerhalb einer unveränderlichen Hülle:
Speichere
java.util.Date(veränderlich) ohne Kopie → Änderungen über die Referenz sind sichtbar. Verwendejava.time(unveränderlich). - Vererbung von einer unveränderlichen Klasse:
Ein Subclass kann einen veränderlichen Zustand/Methoden hinzufügen → bricht die Garantie. Ergebnis: Mache die Basisklasse
final(oder verwendeprivateKonstruktor + Factorys). - Verstoß gegen equals/hashCode bei „Mutation“:
Wenn ein Objekt als Schlüssel in
HashMapverwendet wird, muss seinhashCodestabil bleiben. Daher: Unveränderlichkeit.
Unveränderlichkeit und record (Java 16+)
record ist ein syntaktischer Zucker für „Daten“. Beispiel:
public record Point(int x, int y) {}Besonderheiten:
- Alle Komponenten sind
final, Getter werden generiert,equals/hashCode/toStringsind bereits vorhanden. - Aber: Unveränderlichkeit ist oberflächlich. Wenn ein Feld eine Collection/Array/jeder veränderliche Typ ist, benötigst du eine defensive Kopie im kompakten Konstruktor:
public record Team(String name, List<String> members) {
public Team {
members = List.copyOf(members); // erstellen eines Snapshots
}
}Unveränderlichkeit und Lombok
@Value→ macht die Klassefinal, Felderprivate final, Getter,equals/hashCode.- Sorge trotzdem selbst für eine defensive Kopie für Arrays/Collections/
Date.
@lombok.Value
public class UserDto {
String name;
List<String> roles; // im Konstruktor selbst List.copyOf(...) erstellen
}„Funktionale Updates“: with*-Methoden
In der unveränderlichen Modell werden Änderungen durch die Erstellung eines neuen Objekts durchgeführt:
public Money withCurrency(String newCurrency) {
if (this.currency.equals(newCurrency)) return this; // Mikrooptimierung
return new Money(newCurrency, this.minorUnits, this.tags);
}Das ist bequem und sicher für Multithreading.
Serialisierung unveränderlicher Klassen
- Werden normal serialisiert/deserialisiert (Java/JSON/Protobuf).
- Wenn ein Singleton benötigt wird, füge
readResolve()hinzu. - Bei JSON-Deserialisierung (Jackson) verwende:
- vollständige Konstruktor oder
@JsonCreator+@JsonProperty.
Warum String unveränderlich in Java ist
- Sicherheit: Strings werden oft als Identifikatoren, Klassennamen, Ressourcennamen, Parameter von ClassLoader und SQL-Strings verwendet. Wenn man eine String „am Ort“ ändern könnte, würde dies eine Vielzahl von Sicherheitslücken öffnen (Pfadmanipulation, Klassennamenmanipulation, SQL-Injection auf Objektniveau usw.).
- String Pool (Internierung) Literale Strings werden im Pool gespeichert: identische Literale verweisen auf ein Objekt zur Speichereinsparung und zur Beschleunigung des
==-Vergleichs zwischen Literalen. Der Pool ist nur möglich, wenn Strings unveränderlich sind (andernfalls würde eine Änderung eines Strings alle Referenzen darauf ändern). - Caching von
hashCode:String.hashCode()wird einmal berechnet und in einem Feld zwischengespeichert. Wenn ein String verändert werden könnte, würde der Cache ungültig werden → Verletzung der Verträge vonMap/Set. - Thread-Sicherheit und Sharing: Ein identischer String kann frei zwischen Threads geteilt werden, ohne Synchronisierung.
- JVM/JIT-Optimierungen: Unveränderliche Objekte lassen sich leichter optimieren (Escape-Analysis, konstante Inline, konstante Zusammenfassung).
Fazit: Unveränderlichkeit ist das Fundament von String für Sicherheit, Leistung und Vorhersagbarkeit.
Mini-Beispiele: richtig/falsch
❌ Falsch (Leck auf internem Array)
public final class Bad {
private final int[] data;
public Bad(int[] data) { this.data = data; } // keine Kopie!
public int[] getData() { return data; } // gibt eine lebende Referenz zurück!
}✅ Richtig
public final class Good {
private final int[] data;
public Good(int[] data) { this.data = data.clone(); }
public int[] data() { return data.clone(); }
}✅ Richtig für Date: verwende java.time
public final class Booking {
private final java.time.Instant from;
private final java.time.Instant to;
public Booking(Instant from, Instant to) {
this.from = from; this.to = to; // Instant ist unveränderlich
}
}Tiefe Unveränderlichkeit
Wenn ein Feld eine Liste von Address-Objekten ist und Address veränderlich ist, dann:
- Mache
Addressunveränderlich (bevorzugt), oder - Erstelle eine tiefe Kopie der Elemente und hülle die Liste in
List.copyOfein.
public record Address(String city, String street) {}
public final class Person {
private final List<Address> addresses;
public Person(List<Address> addresses) {
this.addresses = List.copyOf(addresses); // Elemente sind bereits unveränderlich
}
public List<Address> addresses() { return addresses; }
}Best Practices (kurze Checkliste)
- Mache die Klasse
final+ alle Felderprivate final. - Keine Setter. „Änderungen“ erfolgen über
with*-Methoden, die ein neues Objekt erstellen. - Verwende eine defensive Kopie für Arrays/Collections/veränderliche Typen sowohl beim Eingang als auch beim Ausgang.
- Verwende
java.timeanstelle vonDate/Calendar. - Für Collections:
List.copyOf/Map.copyOf(oderCollections.unmodifiableXxx+ vorherige Kopie). - Vermeide
this-Referenzen im Konstruktor (keine Callbacks/Listener dort). - Speichere Invarianz an einem Ort – im Konstruktor/Factory, überprüfe
requireNonNull, Bereiche usw. - Für die Leistung kannst du abgeleitete Werte zwischenspeichern (z.B.
hashCode) – dies ist sicher, wenn das Objekt unveränderlich ist.