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ść
|
|
|
|---|---|---|
| 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.
GO TO FULL VERSION