CodeGym /Kurse /JAVA 25 SELF /CopyOnWrite-Kollektionen, Unmodifiable-Wrapper

CopyOnWrite-Kollektionen, Unmodifiable-Wrapper

JAVA 25 SELF
Level 34 , Lektion 2
Verfügbar

1. Unmodifiable-Wrapper: Wrapper für Collections

Manchmal gibt es im Code bereits eine Collection, die jemand aus Versehen (oder weniger zufällig) ändern könnte. Zum Beispiel haben Sie eine Liste von Benutzern, die Sie nach außen zurückgeben möchten, aber niemand soll sie modifizieren:

List<String> users = new ArrayList<>();
users.add("Alice");
users.add("Bob");

Sie geben diese Liste aus einer Methode zurück, und jemand führt users.add("Hacker") aus – und schon gibt es in Ihrem System einen neuen nicht autorisierten Benutzer! Wie schützt man sich?

Wrapper aus Collections

In Java gibt es seit Langem spezielle Wrapper-Methoden in der Klasse Collections:

  • Collections.unmodifiableList(list)
  • Collections.unmodifiableSet(set)
  • Collections.unmodifiableMap(map)

Diese Methoden liefern einen Wrapper über Ihrer Collection zurück, der keine Änderungen über sich zulässt. Ein Versuch, über den Wrapper ein Element hinzuzufügen, zu entfernen oder zu ändern, löst eine UnsupportedOperationException aus.

Beispiel:

import java.util.*;

public class Demo {
    public static void main(String[] args) {
        List<String> modifiable = new ArrayList<>();
        modifiable.add("Alice");
        modifiable.add("Bob");

        // Unveränderlichen Wrapper erstellen
        List<String> unmodifiable = Collections.unmodifiableList(modifiable);

        System.out.println(unmodifiable); // [Alice, Bob]

        // Versuchen wir, über den Wrapper ein Element hinzuzufügen
        try {
            unmodifiable.add("Charlie"); // Zack! UnsupportedOperationException
        } catch (UnsupportedOperationException e) {
            System.out.println("Die Collection darf nicht geändert werden: " + e);
        }
    }
}

Wichtig!

  • Der Wrapper macht die ursprüngliche Collection NICHT unveränderlich. Wenn jemand noch eine Referenz auf die ursprüngliche Collection hat, kann er sie weiterhin ändern.
  • Alle Änderungen an der ursprünglichen Collection sind sichtbar durch den Wrapper.
modifiable.add("Charlie");
System.out.println(unmodifiable); // [Alice, Bob, Charlie]

Das heißt: Wenn irgendwo im Code jemand der ursprünglichen Liste ein Element hinzufügt/entfernt, sieht der Wrapper das. Das ist kein „Einfrieren“, sondern nur ein Verbot der Änderung über den Wrapper selbst.

Wrapper für andere Collections

Genauso kann man Wrapper für Set, Map und sogar für exotischere Strukturen erstellen:

Set<Integer> numbers = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> unmodSet = Collections.unmodifiableSet(numbers);

Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
Map<String, Integer> unmodMap = Collections.unmodifiableMap(ages);

Vergleich mit Fabrikmethoden (List.of usw.)

  • List.of(...) erstellt eine neue unveränderliche Collection, der man von vornherein nichts hinzufügen kann.
  • Collections.unmodifiableList(list) – das ist ein Wrapper über einer existierenden Collection. Wenn sich die ursprüngliche Liste ändert, ändert sich auch der Wrapper.

Tabelle: Vergleich der Ansätze

List.of(...)
Collections.unmodifiableList(list)
Kann man Elemente hinzufügen? Nein Nein (über den Wrapper)
Kann man zur ursprünglichen hinzufügen? Nicht anwendbar Ja
Sind Änderungen sichtbar? Nein Ja
Ist null zulässig? Nein (NPE) Ja (falls die ursprüngliche Collection es zulässt)
Implementierung Eigene Wrapper über Ihrer Collection

2. CopyOnWrite-Kollektionen

In nebenläufigen Programmen tritt oft die Aufgabe auf: Ein Thread (oder mehrere) liest eine Collection, und ein anderer (oder mehrere) ändert sie gelegentlich. Gewöhnliche Collections sind hier ungeeignet: Es kann zu Race-Conditions, Fehlern, ConcurrentModificationException und weiteren „Freuden“ der Nebenläufigkeit kommen.

Für solche Fälle wurden CopyOnWrite-Collections erfunden – sie sind speziell für Szenarien gedacht, in denen häufig gelesen und selten geändert wird.

Wie funktioniert das?

  • Bei jeder Änderung (Hinzufügen, Entfernen, Ersetzen) erstellt die Collection eine neue Kopie des internen Arrays.
  • Alle lesenden Threads erhalten ihre „eigene“ Version des Arrays, die sich während des Lesens nicht ändert.
  • Das macht das Lesen absolut sicher und erfordert keine Synchronisation.

Hauptklassen

  • CopyOnWriteArrayList<E>
  • CopyOnWriteArraySet<E>

Sie befinden sich im Paket java.util.concurrent.

Beispiel

import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
        cowList.add("Alpha");
        cowList.add("Beta");

        // Sicher iterieren, selbst wenn parallel jemand Elemente hinzufügt
        for (String s : cowList) {
            System.out.println(s);
            cowList.add("Gamma"); // Löst keine ConcurrentModificationException aus!
        }

        System.out.println(cowList); // [Alpha, Beta, Gamma, Gamma]
    }
}

Besonderheiten:

  • Der Iterator von CopyOnWrite-Collections „sieht“ immer einen Schnappschuss der Collection zum Zeitpunkt seiner Erstellung.
  • Wenn nach der Erstellung des Iterators jemand Elemente hinzufügt, sieht der Iterator sie nicht.
  • Man kann während der Iteration sicher Elemente hinzufügen/entfernen – keine ConcurrentModificationException!

Wann sollte man CopyOnWrite-Kollektionen verwenden?

Sie eignen sich für Situationen, in denen viele Threads laufen, die überwiegend Daten aus der Collection lesen, während Änderungen sehr selten sind. Ein klassisches Beispiel – eine Liste von Ereignis-Listenern (event listeners): Neue Listener werden selten hinzugefügt oder entfernt, aber das Benachrichtigen dieser Listener passiert ständig.

Beispiel – Event-Abonnenten

import java.util.concurrent.CopyOnWriteArrayList;

public class EventBus {
    private final CopyOnWriteArrayList<Runnable> listeners = new CopyOnWriteArrayList<>();

    public void subscribe(Runnable listener) {
        listeners.add(listener);
    }

    public void publishEvent() {
        for (Runnable listener : listeners) {
            listener.run(); // sicher, selbst wenn sich gerade jemand an- oder abmeldet!
        }
    }
}

Nachteile von CopyOnWrite-Kollektionen

  • Langsam bei häufigen Änderungen: Jede Änderung erstellt eine neue Kopie des Arrays, was zeit- und speicherintensiv ist.
  • Ineffizient für große Collections: Wenn die Collection groß ist, ist das Kopieren des Arrays eine kostspielige Operation.

3. Vergleich: wann was verwenden?

Unmodifiable-Wrapper (Collections.unmodifiable...)

Wann verwenden: Wenn bereits eine Collection existiert, die Sie vor Änderungen durch externen Code schützen möchten, interne Änderungen (durch den Besitzer der Collection) aber zulässig sind.

Threadsicherheit: Nicht garantiert! Wenn die ursprüngliche Collection aus einem anderen Thread geändert wird, kann es zu Race-Conditions und Fehlern kommen.

Fabrikmethoden (List.of, Set.of, Map.of)

Wann verwenden: Wenn Sie sofort eine konstante, unveränderliche Collection erstellen möchten, ohne Änderungsmöglichkeit von irgendwoher.

Threadsicherheit: Garantiert (die Collection ändert sich überhaupt nicht).

CopyOnWrite-Kollektionen

Wann verwenden: In nebenläufigen Szenarien mit vielen Lesezugriffen und wenigen Änderungen, z. B. für Listener-Listen.

Threadsicherheit: Ja, vollständig threadsicher.

Unveränderlichkeit: Nein, die Collection kann geändert werden, aber jedes Mal wird eine neue Kopie erstellt, sodass Leser nicht beeinträchtigt werden.

4. Typische Fehler und Implementierungsdetails

Fehler Nr. 1: Erwartung einer „Einfrierung“ der ursprünglichen Collection durch den Wrapper. Viele denken, dass Collections.unmodifiableList(list) die Collection vollständig unveränderlich macht. In Wirklichkeit kann jemand mit einer Referenz auf die originäre Liste sie weiterhin ändern, und diese Änderungen sind durch den Wrapper sichtbar. Lösung: Wenn echte Unveränderlichkeit benötigt wird, verwenden Sie List.copyOf(list) (Java 10+) oder List.of(...).

Fehler Nr. 2: CopyOnWrite für eine häufig veränderte Collection verwenden. Wenn in CopyOnWriteArrayList ständig Elemente hinzugefügt oder entfernt werden, führt das zu Performance- und Speicherproblemen. CopyOnWrite ist nur für Szenarien „viele Leser, wenige Schreiber“ geeignet.

Fehler Nr. 3: Erwartung, dass Wrapper threadsicher sind. Collections.unmodifiableList macht eine Collection nicht threadsicher! Wenn die ursprüngliche Liste aus verschiedenen Threads geändert wird, sind Fehler möglich.

Fehler Nr. 4: Collections aus List.of oder Set.of mit null verwenden. Im Gegensatz zu gewöhnlichen Collections lassen Fabrikmethoden kein null zu – der Versuch, eine Collection mit null zu erstellen oder hinzuzufügen, führt zu einer NullPointerException.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION