1. Problem mutowalności kolekcji
W Javie kolekcje przypominają magazyn z towarem: każdy może przyjść i coś dodać, usunąć, zmienić. Czasem to wygodne, ale w dużych programach zamienia się w ból głowy. Wyobraź sobie, że przekazałeś na zewnątrz listę produktów ze swojej klasy, a ktoś wziął i skasował połowę pozycji. Albo – co jeszcze zabawniejsze – w programie wielowątkowym jeden wątek dodaje elementy, a drugi je czyta: wynik może być nieoczekiwany, a błędy – trudne do wychwycenia (np. ConcurrentModificationException).
Oto przykład, dlaczego mutowalność kolekcji jest źródłem błędów:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("Herbata");
products.add("Kawa");
}
public List<String> getProducts() {
// NIEBEZPIECZNE! Zwracamy referencję do wewnętrznej listy
return products;
}
}
public class Main {
public static void main(String[] args) {
Inventory inv = new Inventory();
List<String> external = inv.getProducts();
external.remove("Herbata"); // Ups! Teraz w inwentarzu nie ma herbaty
System.out.println(inv.getProducts()); // [Kawa]
}
}
Zauważyłeś haczyk? Jedna metoda zwraca wewnętrzną kolekcję, inna ją zmienia. Tak można przypadkiem zniszczyć dane, które powinny być chronione.
2. Tworzenie kolekcji niemodyfikowalnych: Collections.unmodifiable*
Aby uniknąć takich wpadek, Java oferuje skuteczną ochronę: kolekcję można uczynić „niemodyfikowalną” za pomocą specjalnych wrapperów z klasy Collections:
- Collections.unmodifiableList(list)
- Collections.unmodifiableSet(set)
- Collections.unmodifiableMap(map)
Jak to działa? Najpierw tworzysz zwykłą kolekcję, a potem owijasz ją „niemodyfikowalną” powłoką:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("Herbata");
drinks.add("Kawa");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
System.out.println(immutableDrinks); // [Herbata, Kawa]
// Spróbujmy dodać element
immutableDrinks.add("Kakao"); // Bum! UnsupportedOperationException
}
}
Próba zmiany takiej kolekcji prowadzi do wyrzucenia wyjątku UnsupportedOperationException. To tak, jakbyś nakleił na pudełko ogromną naklejkę „NIE DOTYKAĆ!” – i każdy, kto spróbuje coś dodać lub usunąć, dostanie po łapach (albo po stosie wywołań).
Przykład: chronimy stan wewnętrzny
Naprawmy naszą klasę Inventory z poprzedniego przykładu:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("Herbata");
products.add("Kawa");
}
public List<String> getProducts() {
// Teraz zwracamy wrapper
return Collections.unmodifiableList(products);
}
}
Teraz, jeśli ktoś spróbuje zmienić otrzymaną listę, dostanie wyjątek.
3. Zachowanie kolekcji niemodyfikowalnych: płytka ochrona
Ważne jest zrozumieć: unmodifiableList i jego „rodzeństwo” tworzą jedynie otoczkę wokół kolekcji źródłowej. Nie tworzą kopii – wszelkie zmiany kolekcji źródłowej (tej, która jest „w środku”) będą widoczne także w wrapperze!
Demonstracja
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("Herbata");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
drinks.add("Kawa"); // Modyfikujemy kolekcję źródłową
System.out.println(immutableDrinks); // [Herbata, Kawa] – element się pojawił!
}
}
Wniosek: wrapper chroni tylko przed zmianami dokonywanymi przez sam wrapper. Jeśli ktoś trzyma referencję do kolekcji źródłowej, nadal może ją zmieniać.
4. Głęboka niemodyfikowalność: mity i rzeczywistość
Wrapery unmodifiable* czynią kolekcję niemodyfikowalną tylko z zewnątrz. Ale jeśli kolekcja zawiera mutowalne obiekty, można je zmieniać!
Przykład
import java.util.*;
class Product {
String name;
Product(String name) {
this.name = name;
}
public String toString() {
return name;
}
}
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Herbata"));
List<Product> immutableProducts = Collections.unmodifiableList(products);
// Zmieniamy obiekt wewnątrz kolekcji
immutableProducts.get(0).name = "Kawa";
System.out.println(immutableProducts); // [Kawa]
}
}
Wniosek:
- Kolekcja jest „niemodyfikowalna”, ale obiekty w środku – już nie.
- Dla pełnej (głębokiej) niemutowalności używaj obiektów niemutowalnych (np. String, Integer, klasy typu record albo twórz własne klasy immutable).
5. Kiedy używać kolekcji niemodyfikowalnych
Aby chronić stan wewnętrzny
Jeśli piszesz klasę, która przechowuje kolekcję, i zwracasz ją na zewnątrz, zawsze zwracaj wrapper, aby nikt nie mógł przypadkowo (lub celowo) zmienić twoich danych:
public List<String> getProducts() {
return Collections.unmodifiableList(products);
}
W programach wielowątkowych
W aplikacjach wielowątkowych mutowalne kolekcje to źródło problemów (race condition, ConcurrentModificationException i inne „uroki życia”). Jeśli kolekcji nie trzeba zmieniać po utworzeniu – zrób ją niemodyfikowalną.
Do przekazywania danych między warstwami
Jeśli przekazujesz kolekcję z jednej warstwy programu do innej (np. z DAO do serwisu), przekazuj niemodyfikowalną kopię lub wrapper – to chroni przed przypadkowymi zmianami.
6. Praktyczne przykłady
Przykład 1: chronimy listę studentów
import java.util.*;
public class Group {
private final List<String> students = new ArrayList<>();
public void addStudent(String name) {
students.add(name);
}
public List<String> getStudents() {
return Collections.unmodifiableList(students);
}
}
Teraz nikt nie będzie mógł dodać ani usunąć studenta bezpośrednio przez getStudents().
Przykład 2: niemodyfikowalna mapa (Map)
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> grades = new HashMap<>();
grades.put("Wasia", 5);
grades.put("Masza", 4);
Map<String, Integer> immutableGrades = Collections.unmodifiableMap(grades);
// immutableGrades.put("Piotr", 3); // UnsupportedOperationException
}
}
7. Przydatne niuanse
Nowoczesne alternatywy: List.of, Set.of, Map.of
W Javie 9 pojawiły się jeszcze wygodniejsze sposoby tworzenia kolekcji niemodyfikowalnych:
List<String> drinks = List.of("Herbata", "Kawa");
Set<String> fruits = Set.of("Jabłko", "Banan");
Map<String, Integer> ages = Map.of("Wasia", 20, "Masza", 21);
- Te kolekcje są niemodyfikowalne (każda próba zmiany – wyjątek).
- Nie mają „źródłowej” mutowalnej kolekcji (w przeciwieństwie do Collections.unmodifiable*).
- Nie dopuszczają wartości null.
Od Javy 10 są też metody kopiujące: List.copyOf, Set.copyOf, Map.copyOf — tworzą niemodyfikowalną kopię przekazanej kolekcji.
Porównanie sposobów tworzenia kolekcji niemodyfikowalnych
| Sposób | Głęboka niemutowalność | Czy można zmieniać kolekcję źródłową? | Czy dopuszcza null? | Wersja Javy |
|---|---|---|---|---|
|
Nie | Tak | Tak | 1.2 |
|
Nie | Nie (brak kolekcji źródłowej) | Nie | 9+ |
8. Typowe błędy przy pracy z kolekcjami niemodyfikowalnymi
Błąd nr 1: Zmiana kolekcji źródłowej po utworzeniu wrappera. Utworzyłeś unmodifiableList, a potem ktoś zmienia listę źródłową. Wrapper przed tym nie ochroni – zmiany będą widoczne we wszystkich miejscach, gdzie używany jest wrapper.
Błąd nr 2: Oczekiwanie głębokiej niemutowalności. Wiele osób sądzi, że skoro kolekcja jest niemodyfikowalna, to obiektów w niej również nie da się zmienić. W rzeczywistości chroniona jest tylko struktura (dodawanie/usuwanie/modyfikacja przez kolekcję), a nie zawartość obiektów.
Błąd nr 3: Używanie kolekcji niemodyfikowalnych z wartością null w nowoczesnych fabrykach. Kolekcje tworzone przez List.of, Set.of, Map.of nie dopuszczają null. Próba dodania lub uzyskania null zakończy się wyjątkiem.
Błąd nr 4: Przekazywanie na zewnątrz referencji do mutowalnych kolekcji. Jeśli zwracasz na zewnątrz referencję do wewnętrznej kolekcji (bez wrappera), tracisz kontrolę nad swoimi danymi – prosta droga do błędów i „rozszczelnienia” inwariantów.
Błąd nr 5: Używanie kolekcji niemodyfikowalnych w kodzie, który oczekuje mutowalności. Jeśli obcy kod spróbuje zmienić kolekcję (np. dodać element), dostanie UnsupportedOperationException. Upewnij się, że konsumenci wiedzą o niemodyfikowalności danych.
GO TO FULL VERSION