W3docs

Best Practices für Unveränderlichkeit in Java

Warum Unveränderlichkeit in Java eine gute Standardwahl ist und wie man wirklich unveränderliche Typen sicher erstellt.

Ein unveränderliches Objekt ist eines, dessen Zustand sich nach der Erstellung nicht mehr ändern kann. Diese eine Eigenschaft beseitigt eine ganze Klasse von Fehlern: Es gibt keine unerwarteten Mutationen durch einen anderen Thread, keine Aliasing-Überraschungen, bei denen zwei Variablen denselben Zustand teilen, und keinen Bedarf für defensive Kopien bei jedem Lesezugriff. In Java ist Unveränderlichkeit nicht automatisch — sie muss bewusst eingebaut werden. Dieses Kapitel behandelt die Muster, die einen Typ wirklich unveränderlich machen, und die Gewohnheiten, die ihn so erhalten.

Warum Unveränderlichkeit eine gute Standardwahl ist

Veränderlicher gemeinsamer Zustand ist die Wurzel der meisten Nebenläufigkeitsfehler und einer überraschenden Anzahl von Fehlern in Einzelthread-Programmen. Wenn ein Objekt sich nicht ändern kann, kann man es frei weitergeben, cachen und darüber nachdenken, ohne zu verfolgen, wer noch eine Referenz darauf hält.

EigenschaftVeränderliches ObjektUnveränderliches Objekt
Thread-SicherheitBenötigt Sperren oder SorgfaltVon Natur aus thread-sicher
Sicher weiterzugebenNein — Aufrufer können mutierenJa — dieselbe Instanz herausgeben
Sicher als Map-SchlüsselRiskant — hashCode kann sich ändernJa — Identität ist stabil
CachingMuss bei Änderung invalidiert werdenUnbegrenzt cachebar
NachvollziehbarkeitJeden Schreibzugriff verfolgenWert ist bei der Erstellung festgelegt

Der Preis ist die Allokation: Das Ändern eines Felds bedeutet, ein neues Objekt zu erstellen. Für den Großteil des Codes ist dieser Aufwand vernachlässigbar und die Sicherheit ist es wert. Greifen Sie nur dann auf Veränderlichkeit zurück, wenn Profiling beweist, dass Sie sie brauchen.

Die fünf Regeln für eine unveränderliche Klasse

Eine Klasse ist unveränderlich, wenn alle folgenden Bedingungen erfüllt sind. Wenn eine davon fehlt, kann ein Aufrufer den Zustand von außen ändern.

  1. Die Klasse ist final (oder alle Konstruktoren sind privat), damit sie nicht durch veränderliches Verhalten überschrieben werden kann.
  2. Alle Felder sind private final.
  3. Es gibt keine Setter — und keine anderen Methoden, die ein Feld ändern.
  4. Veränderliche Felder werden beim Einlesen defensiv kopiert, damit die Referenz des Aufrufers nicht verwendet werden kann, um den internen Zustand zu ändern.
  5. Getter legen niemals ein veränderliches internes Objekt direkt offen — geben Sie eine Kopie oder eine nicht änderbare Ansicht zurück.
public final class Money {
    private final long cents;
    private final String currency;

    public Money(long cents, String currency) {
        this.cents = cents;
        this.currency = currency;
    }

    public long cents() { return cents; }
    public String currency() { return currency; }

    // "Change" returns a new object instead of mutating this one.
    public Money plus(Money other) {
        return new Money(this.cents + other.cents, currency);
    }
}

Defensive Kopien für veränderliche Felder

Primitive Typen und String sind bereits unveränderlich, daher ist deren Speicherung sicher. Die Gefahr liegt bei veränderlichen Feldern — Arrays, Collections, Dates. Wenn Sie die Referenz des Aufrufers direkt speichern, behält er einen Zugang zu Ihren Interna.

public final class Schedule {
    private final List<String> slots;

    public Schedule(List<String> slots) {
        // Copy IN: the caller can't mutate our list later.
        this.slots = List.copyOf(slots);
    }

    public List<String> slots() {
        // copyOf already returns an unmodifiable list, so this is safe to hand out.
        return slots;
    }
}

List.copyOf, Set.copyOf und Map.copyOf (Java 10+) erledigen beide Aufgaben auf einmal: Sie kopieren die Daten und geben eine nicht änderbare Ansicht zurück. Für Arrays verwenden Sie array.clone() beim Einlesen und clone() erneut beim Ausgeben, da Arrays immer veränderlich sind und keinen schreibgeschützten Wrapper haben.

Records: Unveränderlichkeit durch Konstruktion

Ein record (Java 16+) ist die prägnanteste Art, einen unveränderlichen Datenträger zu deklarieren. Der Compiler generiert private final-Felder, einen kanonischen Konstruktor, Accessors und wertbasierte equals/hashCode/toString.

public record Point(int x, int y) {
    // Compact constructor for validation and defensive copying.
    public Point {
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException("coordinates must be non-negative");
        }
    }
}

Records decken den Standardfall hervorragend ab, sind aber kein magischer Schutz: Wenn eine Record-Komponente ein veränderlicher Typ ist (wie List), muss man ihn im kompakten Konstruktor trotzdem defensiv kopieren, da der generierte Accessor die gespeicherte Referenz unverändert zurückgibt.

Geänderte Kopien erzeugen: das „Wither"-Muster

Da man ein unveränderliches Objekt nicht mutieren kann, erstellt man eine modifizierte Kopie. Die Konvention ist eine withX-Methode, die eine neue Instanz mit einem geänderten Feld und den restlichen übernommenen Werten zurückgibt.

public final class User {
    private final String name;
    private final String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public User withEmail(String newEmail) {
        return new User(this.name, newEmail); // new object, original untouched
    }
}

Dadurch bleibt das Original sicher teilbar, während Aufrufer Varianten erstellen können. Das ist dasselbe Modell, das das JDK intern verwendet — LocalDate.plusDays, String.replace und BigDecimal.add geben alle neue Instanzen zurück, anstatt den Empfänger zu mutieren.

Ein vollständiges ausführbares Beispiel

Das folgende Programm erstellt einen kleinen unveränderlichen Account und versucht dann jeden Trick, den ein Aufrufer verwenden könnte, um ihn zu mutieren — eine Liste übergeben und das Original mutieren, die zurückgegebene Liste mutieren und umbenennen. Es beweist, dass jede Verteidigung standhält, und zeigt dann, warum unveränderliche Werte sichere Map-Schlüssel sind.

java— editable, runs on the server

Was der Lauf zeigt:

  • Roles after mutating source: [read, write] beweist, dass die defensive Kopie funktioniert hat — das Hinzufügen von admin zur ursprünglichen Liste hat den Account nie erreicht.
  • Die UnsupportedOperationException bei acc.roles().add("hacker") zeigt, dass der Getter eine nicht änderbare Ansicht zurückgegeben hat, sodass Aufrufer Interna nicht durch sie ändern können.
  • Original name still: Ada neben New object name: Grace beweist, dass withName eine Kopie erstellt und das Original unverändert gelassen hat.
  • Different instance: true bestätigt, dass der Wither ein tatsächlich neues Objekt zurückgegeben hat und nicht dieselbe Referenz.
  • Records equal by value: true und Key still found: origin-ish zeigen, dass unveränderliche Werte nach Inhalt verglichen und gehasht werden, was sie zu zuverlässigen HashMap-Schlüsseln macht.

Übung

Übung
Warum muss man eine defensive Kopie im Konstruktor erstellen, wenn eine unveränderliche Klasse ein veränderliches Feld wie eine List speichert?
Warum muss man eine defensive Kopie im Konstruktor erstellen, wenn eine unveränderliche Klasse ein veränderliches Feld wie eine List speichert?
Was this page helpful?