1. Wprowadzenie
Mówiąc najprościej, kolekcja mutable to taka, którą można zmieniać po utworzeniu: dodawać, usuwać i modyfikować elementy. Kolekcja immutable (niemodyfikowalna) to kolekcja, której po utworzeniu nie da się zmienić. Jak beton po związaniu: można patrzeć i dotykać, ale lepić nowych figurek już się nie da.
Kolekcja mutable to notes z ołówkiem: piszesz, ścierasz, dodajesz nowe notatki. Kolekcja immutable to zalaminowana strona: teraz nikt nie dopisze ani nie wymaże niczego.
Przykłady modyfikowalnych (mutable) kolekcji
W Java praktycznie wszystkie standardowe kolekcje domyślnie są modyfikowalne. To takie klasy jak:
- ArrayList
- LinkedList
- HashSet
- TreeSet
- HashMap
- LinkedHashMap
- i wiele innych
Przykład: ArrayList
import java.util.*;
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.set(1, "Charlie"); // zamieniono Bob na Charlie
names.remove("Alice"); // usunięto Alice
System.out.println(names); // [Charlie]
Tutaj możemy robić z kolekcją, co tylko chcemy: dodawać, usuwać, zamieniać elementy miejscami. To wygodne, gdy kolekcja buduje się dynamicznie, na przykład podczas czytania danych z pliku lub wejścia użytkownika.
2. Przykłady niemodyfikowalnych (immutable) kolekcji
Te „nowe twarze”, które pojawiły się w Java 9, zapewne już znasz, ale warto się do nich przyzwyczaić:
- List.of(...)
- Set.of(...)
- Map.of(...)
- List.copyOf(collection)
- Set.copyOf(collection)
- Map.copyOf(map)
Przykład: List.of
List<String> planets = List.of("Mercury", "Venus", "Earth", "Mars");
System.out.println(planets); // [Mercury, Venus, Earth, Mars]
planets.add("Jupiter"); // Rzuci UnsupportedOperationException!
Próba zmiany kolekcji kończy się wyjątkiem w czasie wykonywania.
Przykład: Collections.unmodifiableList
List<String> modifiable = new ArrayList<>(List.of("a", "b"));
List<String> unmodifiable = Collections.unmodifiableList(modifiable);
unmodifiable.add("c"); // UnsupportedOperationException!
Ale jest haczyk: jeśli zmienisz kolekcję źródłową, „owijka” również się zmieni!
modifiable.add("c");
System.out.println(unmodifiable); // [a, b, c] — element się pojawił!
3. Kluczowe różnice między kolekcjami mutable i immutable
| Właściwość | Mutable (modyfikowalne) | Immutable (niemodyfikowalne) |
|---|---|---|
| Czy można dodać element? | Tak | Nie |
| Czy można usunąć element? | Tak | Nie |
| Czy można zmienić element? | Tak (np. set) | Nie |
| Bezpieczeństwo wątkowe | Nie (domyślnie) | Tak (brak stanu — nie ma czego zmieniać) |
| Czy można dodać null? | Tak (zwykle) | Nie (w metodach fabrycznych Java 9+) |
| Implementacje | ArrayList, HashSet i inne | List.of, Set.of, Map.of, copyOf |
4. Po co w ogóle są potrzebne kolekcje niemodyfikowalne?
Od razu nasuwa się pytanie: skoro kolekcje modyfikowalne są tak elastyczne, po co nam niemodyfikowalne? Powodów jest naprawdę sporo — wszystkie wiążą się z bezpieczeństwem, czytelnością i przewidywalnością kodu.
Bezpieczeństwo i ochrona przed błędami
Gdy wystawiasz kolekcję na zewnątrz (np. z metody lub klasy), chcesz mieć pewność, że nikt przypadkowo nie zmieni jej zawartości. Szczególnie jest to ważne, jeśli kolekcja zawiera „ważne” dane, które nie powinny się zmieniać po inicjalizacji.
Przykład:
public class Team {
private final List<String> players;
public Team(List<String> players) {
// Tworzymy niemodyfikowalną kopię, aby nikt nie mógł podmienić składu
this.players = List.copyOf(players);
}
public List<String> getPlayers() {
return players;
}
}
Teraz żaden kod, który otrzyma listę graczy, nie będzie mógł dodać tam „kolegi”.
Bezpieczeństwo wątkowe
Kolekcje modyfikowalne nie są bezpieczne przy dostępie z wielu wątków jednocześnie. Kolekcje niemodyfikowalne przeciwnie — można je swobodnie przekazywać między wątkami, nikt ich nie popsuje.
Uproszczenie debugowania
Jeśli kolekcja się nie zmienia, zawsze wiesz, co w niej leży. Nie musisz się obawiać, że ktoś „po cichu” zmienił ją w innym miejscu kodu.
Użycie jako klucze lub wartości w innych kolekcjach
Obiekty niemodyfikowalne są idealnymi kandydatami na klucze w Map lub elementy w Set. Jeśli obiekt może się zmienić po dodaniu, ryzykujesz utratę dostępu do niego (zob. hashCode i equals).
5. Kiedy lepiej używać kolekcji modyfikowalnych?
Kolekcje modyfikowalne są dobre, gdy:
- Kolekcja jest budowana etapami, w pętli lub z różnych źródeł.
- Potrzebne są częste zmiany: dodawanie, usuwanie, sortowanie.
- Kolekcja jest przeznaczona do użytku wewnętrznego i nikt „z zewnątrz” nie może jej zepsuć.
Przykład: Budowanie listy
List<String> shoppingList = new ArrayList<>();
shoppingList.add("Mleko");
shoppingList.add("Chleb");
shoppingList.add("Jabłka");
// Po zbudowaniu — można utworzyć wersję niemodyfikowalną
List<String> finalList = List.copyOf(shoppingList);
6. Kiedy lepiej używać kolekcji niemodyfikowalnych?
- Do przechowywania danych stałych (np. lista dni tygodnia).
- Do przekazywania kolekcji między warstwami aplikacji (np. z DAO do serwisu).
- Do zwracania kolekcji z metod, aby zabezpieczyć je przed zmianami.
- Do scenariuszy wielowątkowych, gdzie liczy się bezpieczeństwo.
Przykład: Dane stałe
public static final List<String> WEEKDAYS = List.of(
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"
);
Przykład: Przekazywanie na zewnątrz
public List<String> getReadOnlyNames() {
return List.copyOf(names); // nikt nie będzie mógł zmienić listy
}
7. Cechy szczególne i pułapki
Niemodyfikowalność ≠ bezpieczeństwo wątkowe
Kolekcja niemodyfikowalna jest chroniona przed zmianami, ale to nie znaczy, że jest odporna na inne problemy w środowisku wielowątkowym (np. jeśli elementy kolekcji są obiektami modyfikowalnymi).
List<List<String>> listOfLists = List.of(new ArrayList<>());
listOfLists.get(0).add("Oops!"); // Można zmienić wewnętrzną listę!
Owijki vs kopie
Jak już było wspomniane wcześniej, Collections.unmodifiableList to owijka i jeśli zmienisz kolekcję źródłową, owijka też się zmieni. Natomiast List.copyOf tworzy prawdziwą, niezależną kopię.
List<String> base = new ArrayList<>(List.of("a", "b"));
List<String> wrap = Collections.unmodifiableList(base);
List<String> copy = List.copyOf(base);
base.add("c");
System.out.println(wrap); // [a, b, c] — zmieniło się!
System.out.println(copy); // [a, b] — zostało bez zmian!
NullPointerException
Metody fabryczne (List.of, Set.of, Map.of) nie pozwalają dodawać null:
List<String> bad = List.of("a", null); // Rzuci NullPointerException już przy tworzeniu!
8. Porównanie podejść: zalety i wady
Kolekcje modyfikowalne
Główną zaletą kolekcji modyfikowalnych jest ich elastyczność. Możesz dodawać i usuwać elementy w locie, przebudowywać kolekcję wraz z rozwojem logiki programu. Takie podejście jest szczególnie wygodne, gdy trzeba szybko złożyć tymczasową strukturę albo coś dynamicznie zmieniać.
Ale za wygodę płaci się cenę. Kolekcja modyfikowalna zawsze niesie ryzyko przypadkowej ingerencji: ktoś w kodzie może niechcący zmienić dane, co prowadzi do trudnych do wychwycenia błędów. W programach wielowątkowych jest jeszcze gorzej — równoległe zmiany łatwo prowadzą do wyścigów i nieoczekiwanych awarii. Kontrolowanie cyklu życia takich danych też bywa trudniejsze: trzeba stale pamiętać, kto i kiedy może kolekcję zmodyfikować.
Kolekcje niemodyfikowalne
Z kolekcjami niemodyfikowalnymi pracuje się spokojniej. Wiesz na pewno: nikt ich nie poprawi „za plecami”. To czyni kod bezpieczniejszym, łatwiejszym do debugowania i testowania, a same kolekcje wygodnie przekazuje się między wątkami bez dodatkowych blokad.
Minusem bywa to, że czasem trzeba poświęcić wydajność. Jeśli trzeba dodać nowy element, trzeba utworzyć nową kopię kolekcji, co pociąga za sobą dodatkowe zużycie pamięci i czasu. Poza tym składanie dużych i złożonych struktur od razu w wersji niemodyfikowalnej bywa niewygodne. Zwykle buduje się je we wstępnej kolekcji modyfikowalnej, a gdy wszystko jest gotowe — „zamraża”.
9. Typowe błędy
Błąd nr 1: Zwracanie na zewnątrz modyfikowalnej kolekcji. Jeśli zwracasz z metody zwykły ArrayList, każdy zewnętrzny kod może dodawać lub usuwać elementy. To może prowadzić do błędów, które bardzo trudno wyśledzić.
Błąd nr 2: Używanie owijki zamiast kopii. Jeśli używasz Collections.unmodifiableList, ale kolekcja źródłowa gdzieś indziej się zmienia, „niemodyfikowalność” jest iluzją.
Błąd nr 3: Modyfikowalne obiekty wewnątrz kolekcji immutable. Nawet jeśli sama kolekcja jest niemodyfikowalna, jej elementy mogą być modyfikowalne. To może prowadzić do nieoczekiwanych zmian stanu.
Błąd nr 4: Próba dodania null do kolekcji utworzonej metodami fabrycznymi. W odróżnieniu od starszych kolekcji, nowe metody fabryczne nie pozwalają dodawać null — skończy się to NullPointerException.
Błąd nr 5: Oczekiwanie konkretnej implementacji. Kolekcje utworzone przez List.of lub Set.of nie gwarantują typu implementacji (to nie musi być ArrayList czy HashSet). Nie polegaj na tym.
GO TO FULL VERSION