CodeGym /Kursy /JAVA 25 SELF /Kolekcje CopyOnWrite, niemodyfikowalne wrappery

Kolekcje CopyOnWrite, niemodyfikowalne wrappery

JAVA 25 SELF
Poziom 34, Lekcja 2
Dostępny

1. Unmodifiable wrappers: wrappery nad kolekcjami

Czasem w kodzie istnieje już kolekcja, którą ktoś może przypadkowo (albo i nie) zmodyfikować. Na przykład masz listę użytkowników, którą chcesz udostępnić na zewnątrz, ale nie chcesz, by ktoś ją modyfikował:

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

Zwracasz tę listę z metody, a ktoś wywołuje users.add("Hacker"); — i już masz w systemie nowego, nieautoryzowanego użytkownika! Jak się przed tym chronić?

Wrappery z Collections

W Javie od dawna są specjalne metody–wrappery w klasie Collections:

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

Te metody zwracają wrapper wokół twojej kolekcji, który nie pozwala jej modyfikować przez siebie. Próba dodania, usunięcia lub zmiany elementu przez wrapper rzuci UnsupportedOperationException.

Przykład:

import java.util.*;

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

        // Tworzymy niemodyfikowalny wrapper
        List<String> unmodifiable = Collections.unmodifiableList(modifiable);

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

        // Spróbujmy dodać element przez wrapper
        try {
            unmodifiable.add("Charlie"); // Bum! UnsupportedOperationException
        } catch (UnsupportedOperationException e) {
            System.out.println("Nie można zmienić kolekcji: " + e);
        }
    }
}

Ważne!

  • Wrapper NIE czyni oryginalnej kolekcji niezmienną. Jeśli ktoś trzyma referencję do oryginalnej kolekcji, nadal może ją zmieniać.
  • Wszystkie zmiany oryginalnej kolekcji są widoczne przez wrapper.
modifiable.add("Charlie");
System.out.println(unmodifiable); // [Alice, Bob, Charlie]

Czyli jeśli ktoś gdzieś w kodzie doda/usunie element w oryginalnej liście, wrapper to zobaczy. To nie jest „zamrożenie”, a jedynie zakaz modyfikacji przez sam wrapper.

Wrappery dla innych kolekcji

Tak samo można tworzyć wrappery dla Set, Map, a nawet bardziej egzotycznych struktur:

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);

Porównanie z metodami fabrycznymi (List.of i in.)

  • List.of(...) tworzy nową niezmienną kolekcję, do której nie można nic dodać nawet na starcie.
  • Collections.unmodifiableList(list) — to wrapper nad istniejącą kolekcją. Jeśli oryginalna lista się zmieni, zmieni się też wrapper.

Tabela: porównanie podejść

List.of(...)
Collections.unmodifiableList(list)
Czy można dodać? Nie Nie (przez wrapper)
Czy można dodać do źródłowej? Nie dotyczy Tak
Czy widać zmiany? Nie Tak
Czy można umieścić null? Nie (NPE) Tak (jeśli kolekcja źródłowa na to pozwala)
Implementacja Własna Wrapper nad twoją kolekcją

2. Kolekcje CopyOnWrite

W programach wielowątkowych często pojawia się zadanie: jeden wątek (lub kilka) czyta kolekcję, a inny (lub inne) czasem ją zmieniają. Zwykłe kolekcje się tu nie nadają: możliwe są warunki wyścigu, błędy, ConcurrentModificationException i inne „przyjemności” świata wielowątkowego.

Dla takich przypadków stworzono kolekcje CopyOnWrite — są zaprojektowane specjalnie do scenariuszy, w których odczytów jest dużo, a zmian mało.

Jak to działa?

  • Przy każdej modyfikacji (dodaniu, usunięciu, zamianie) kolekcja tworzy nową kopię wewnętrznej tablicy.
  • Wszystkie wątki czytające dostają „swoją” wersję tablicy, która nie zmienia się, dopóki ją czytają.
  • Dzięki temu odczyt jest całkowicie bezpieczny i nie wymaga synchronizacji.

Główne klasy

  • CopyOnWriteArrayList<E>
  • CopyOnWriteArraySet<E>

Znajdują się w pakiecie java.util.concurrent.

Przykład użycia

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");

        // Można bezpiecznie iterować, nawet gdy ktoś równolegle dodaje elementy
        for (String s : cowList) {
            System.out.println(s);
            cowList.add("Gamma"); // Nie spowoduje ConcurrentModificationException!
        }

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

Cechy:

  • Iterator kolekcji CopyOnWrite zawsze „widzi” migawkę kolekcji z momentu utworzenia iteratora.
  • Jeśli po utworzeniu iteratora ktoś doda elementy, iterator ich nie zobaczy.
  • Można bezpiecznie dodawać/usuwać elementy w trakcie iteracji — żadnego ConcurrentModificationException!

Kiedy używać kolekcji CopyOnWrite?

Pasują do sytuacji, gdzie w programie działa wiele wątków, które głównie czytają dane z kolekcji, a operacje modyfikacji są bardzo rzadkie. Klasyczny przykład — lista słuchaczy zdarzeń (event listeners): nowych słuchaczy dodaje się lub usuwa nieczęsto, za to powiadamianie tych słuchaczy odbywa się nieustannie.

Przykład — subskrybenci zdarzeń

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(); // bezpieczne, nawet jeśli ktoś właśnie zapisał się/wypisał się!
        }
    }
}

Wady kolekcji CopyOnWrite

  • Wolne przy częstych zmianach: każda modyfikacja to utworzenie nowej kopii tablicy, co jest kosztowne pamięciowo i czasowo.
  • Nieefektywne dla dużych kolekcji: jeśli kolekcja jest duża, kopiowanie tablicy to kosztowna operacja.

3. Porównanie: kiedy czego używać?

Unmodifiable wrappers (Collections.unmodifiable...)

Kiedy używać: Gdy masz już kolekcję, którą chcesz chronić przed modyfikacjami przez kod zewnętrzny, ale zmiany od środka (właściciel kolekcji) są dopuszczalne.

Bezpieczeństwo wątkowe: Nie jest gwarantowane! Jeśli oryginalna kolekcja jest zmieniana z innego wątku, mogą wystąpić warunki wyścigu i błędy.

Metody fabryczne (List.of, Set.of, Map.of)

Kiedy używać: Gdy chcesz od razu utworzyć stałą, niezmienną kolekcję bez możliwości modyfikacji skądkolwiek.

Bezpieczeństwo wątkowe: Gwarantowane (kolekcja w ogóle się nie zmienia).

Kolekcje CopyOnWrite

Kiedy używać: W scenariuszach wielowątkowych, gdzie jest dużo odczytów i mało zmian. Na przykład dla list subskrybentów.

Bezpieczeństwo wątkowe: Tak, w pełni bezpieczne wątkowo.

Niezmienność: Nie — kolekcję można zmieniać, ale za każdym razem powstaje nowa kopia, aby czytelnicy nie ucierpieli.

4. Typowe błędy i szczegóły implementacji

Błąd nr 1: Oczekiwanie „zamrożenia” oryginalnej kolekcji przez wrapper. Wiele osób myśli, że Collections.unmodifiableList(list) czyni kolekcję całkowicie niezmienną. W rzeczywistości, jeśli ktoś zachowa referencję do oryginalnej listy, może ją zmieniać, a te zmiany będą widoczne przez wrapper. Rozwiązanie: Jeśli potrzebna jest prawdziwa niezmienność — użyj List.copyOf(list) (Java 10+) lub List.of(...).

Błąd nr 2: Używanie CopyOnWrite dla często zmieniającej się kolekcji. Jeśli do CopyOnWriteArrayList stale dodaje się lub usuwa elementy, doprowadzi to do problemów z wydajnością i zużyciem pamięci. CopyOnWrite nadaje się tylko do scenariuszy „wielu czytelników, mało piszących”.

Błąd nr 3: Oczekiwanie, że wrappery są bezpieczne wątkowo. Collections.unmodifiableList nie czyni kolekcji bezpieczną wątkowo! Jeśli oryginalna lista jest zmieniana z różnych wątków, możliwe są błędy.

Błąd nr 4: Użycie kolekcji z List.of lub Set.of z null. W przeciwieństwie do zwykłych kolekcji, metody fabryczne nie dopuszczają null — próba dodania lub nawet utworzenia kolekcji z null spowoduje NullPointerException.

1
Zadanie
JAVA 25 SELF, poziom 34, lekcja 2
Niedostępne
Ochrona listy zabronionych słów 🚫
Ochrona listy zabronionych słów 🚫
1
Zadanie
JAVA 25 SELF, poziom 34, lekcja 2
Niedostępne
Dziennik zdarzeń w czasie rzeczywistym 📈
Dziennik zdarzeń w czasie rzeczywistym 📈
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION