W3docs

Unveränderliche Klassen in Java

Unveränderliche Klassen in Java mit final-Feldern, defensiven Kopien und ohne Setter entwerfen.

Eine unveränderliche Klasse ist eine, deren Instanzen nach der Konstruktion nicht mehr verändert werden können. String, Integer, LocalDate, BigDecimal, UUID — Javas Standardbibliothek ist voll davon, und das nicht zufällig. Unveränderliche Objekte sind sicher thread-übergreifend zu teilen, sicher als HashMap-Schlüssel zu verwenden, sicher zu cachen und leicht zu verstehen: Sobald man eines gesehen hat, kennt man seinen Zustand für den Rest seines Lebens.

Eine Klasse unveränderlich zu machen ist nicht eine Frage eines einzelnen Schlüsselworts — es geht darum, eine Handvoll Regeln gemeinsam zu befolgen. Wird eine davon vergessen, erhält man eine Klasse, die unveränderlich aussieht, es aber nicht ist.

Die fünf Regeln

Um eine Klasse wirklich unveränderlich zu machen:

  1. Die Klasse als final deklarieren (oder nur private Konstruktoren verwenden). Andernfalls kann eine Unterklasse den Vertrag brechen.
  2. Jedes Feld private final machen. final verhindert eine erneute Zuweisung nach der Konstruktion; private verhindert, dass Aufrufer direkt darauf zugreifen.
  3. Keine Setter bereitstellen. Jede Mutationsmethode (add, set, clear, reset) ist ausgeschlossen.
  4. Veränderliche Eingaben im Konstruktor defensiv kopieren. Wenn der Aufrufer ein Date oder eine List übergibt, muss eine Kopie erstellt werden — andernfalls kann er sie von außen verändern und das "unveränderliche" Objekt ändert sich unbemerkt.
  5. Veränderliche Rückgaben in Gettern defensiv kopieren — aus dem gleichen Grund in umgekehrter Richtung.

Eine Klasse, die alle fünf Regeln erfüllt, ist tief unveränderlich. Wird auch nur eine davon vernachlässigt, wird die Garantie aufgebrochen.

Warnung

final allein ist keine Unveränderlichkeit. Ein final-Feld kann nicht neu zugewiesen werden, aber wenn es auf ein veränderliches Objekt zeigt — eine List, ein Array, ein Date — kann dieses Objekt sich dennoch ändern. final List<String> tags bedeutet, dass die Liste nicht gegen eine andere ausgetauscht werden kann, nicht dass der Inhalt der Liste eingefroren ist. Die Regeln 4 und 5 existieren genau, um diese Lücke zu schließen. Siehe Java final keyword für das, was final verspricht und was nicht.

Das minimale Beispiel

Für eine Klasse, deren Felder alle primitiv oder bereits unveränderlich sind, reduzieren sich die Regeln auf nahezu nichts:

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

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

  public int x() { return x; }
  public int y() { return y; }
}

int ist ein primitiver Typ, daher gibt es nichts defensiv zu kopieren. Die Klasse ist final, die Felder sind private final, es gibt keine Setter. Fertig.

Veränderliche Felder benötigen defensive Kopien

Das Problem beginnt, wenn ein Feld selbst veränderlich ist — ein Array, ein Date, eine ArrayList. Wenn die Referenz des Aufrufers direkt gespeichert wird, behält er einen Zugriff darauf und kann die internen Daten verändern:

// Broken: the array is shared
public final class Trajectory {
  private final double[] points;
  public Trajectory(double[] points) { this.points = points; }
  public double[] points() { return points; }
}

double[] arr = {1.0, 2.0, 3.0};
Trajectory t = new Trajectory(arr);
arr[0] = 999;                     // mutates the "immutable" object!
System.out.println(t.points()[0]);  // 999

Die Lösung besteht darin, sowohl beim Eingang als auch beim Ausgang zu kopieren:

public final class Trajectory {
  private final double[] points;
  public Trajectory(double[] points) {
    this.points = points.clone();    // copy in
  }
  public double[] points() {
    return points.clone();           // copy out
  }
}

Bei Collections ist das Äquivalent List.copyOf(other) (gibt eine unveränderliche Liste zurück, die durch eine Kopie gesichert ist):

public final class Recipe {
  private final String        name;
  private final List<String>  steps;
  public Recipe(String name, List<String> steps) {
    this.name  = name;
    this.steps = List.copyOf(steps);   // copy + unmodifiable view
  }
  public List<String> steps() { return steps; }   // already unmodifiable
}

Zu beachten ist die Asymmetrie zum Array-Beispiel: clone() eines Arrays erzeugt eine veränderliche Kopie, daher muss beim Ausgang erneut kopiert werden. List.copyOf erzeugt eine unveränderliche Liste, sodass der Getter sie direkt zurückgeben kann — jeder Aufrufer, der versucht, sie zu mutieren, erhält eine UnsupportedOperationException. Unveränderliche Collection-Typen sollten bevorzugt werden, wann immer möglich; sie eliminieren eine ganze Klasse von Kopierfeh­lern beim Ausgang.

„Änderungen" geben neue Instanzen zurück

Eine unveränderliche Klasse kann dennoch Änderungen unterstützen — indem sie eine neue Instanz zurückgibt:

public final class Money {
  private final long cents;
  public Money plus(Money other)  { return new Money(cents + other.cents); }
  public Money times(int factor)  { return new Money(cents * factor); }
  // constructor + accessors omitted
}

Konventionell heißt die Methode with..., wenn sie eine Kopie mit einem geänderten Feld erzeugt: point.withX(5), user.withEmail("..."). Die Java-Datums-/Uhrzeit-API verwendet dieses Muster konsequent — LocalDate.plusDays(7), LocalDate.withYear(2026).

Warum das wichtig ist

Unveränderliche Objekte bieten:

  • Thread-Sicherheit ohne Aufwand. Keine Sperren, kein volatile, keine Sichtbarkeitsüberraschungen — es gibt nichts zu synchronisieren, weil sich der Zustand nicht ändern kann.
  • Sicheres Teilen und Cachen. Zwei Aufrufer, die dasselbe Money(2000, "USD") halten, können sich nicht gegenseitig beeinflussen.
  • Zuverlässige Hash-Schlüssel. Da sich die in hashCode verwendeten Felder nicht ändern können, wird der Bucket des Objekts nie veraltet. Ein veränderlicher Schlüssel, dessen Hash sich nach der Speicherung in einer HashMap ändert, ist praktisch verloren — siehe Java equals and hashCode.
  • Einfacheres Verstehen. Sobald man ein unveränderliches Objekt gesehen hat, weiß man, was es für den Rest seines Lebens tun wird. Keine „Wo wurde das mutiert?"-Archäologie.

Der Preis ist die Allokation neuer Instanzen für jede „Änderung". Bei kleinen, häufig verwendeten Objekten (String, Integer) ist das selten ein Problem; die JVM ist sehr gut bei kurzlebigen Allokationen. Für wirklich aufwändige Fälle gibt es spezifische Techniken (String-Builder, persistente Datenstrukturen) — aber diese sollten nur dann eingesetzt werden, wenn das Profiling ein echtes Problem aufzeigt.

Records erledigen den größten Teil der Arbeit

Ein record ist implizit final, hat private final-Felder, generiert Accessors ohne Setter und bietet equals/hashCode/toString kostenlos:

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

Das ist tief unveränderlich, solange die Komponenten selbst unveränderlich sind. Bei Records, die eine veränderliche Komponente (eine List, ein Array) enthalten, wird dennoch ein kompakter Konstruktor benötigt, der defensiv kopiert:

public record Recipe(String name, List<String> steps) {
  public Recipe {
    steps = List.copyOf(steps);
  }
}

Wenn Records passen, sind sie der kürzeste Weg zu einer korrekten unveränderlichen Klasse.

Ein ausgearbeitetes Beispiel

java— editable, runs on the server

Wie es weitergeht

Unveränderliche Klassen handeln von der Kontrolle über Änderungen. Das letzte Kapitel von Teil 6 handelt von der Kontrolle über die Anzahl — eine Klasse, die so gestaltet ist, dass immer nur eine Instanz existiert. Weiter zu Java singleton pattern.

Übungen

Übung
Warum ist eine defensive Kopie im Konstruktor einer unveränderlichen Klasse notwendig, die eine veränderliche Eingabe wie eine `List` oder ein Array enthält?
Warum ist eine defensive Kopie im Konstruktor einer unveränderlichen Klasse notwendig, die eine veränderliche Eingabe wie eine `List` oder ein Array enthält?
Was this page helpful?