CodeGym /Kursy /JAVA 25 SELF /Kolekcje mutable vs immutable: różnice i zastosowania

Kolekcje mutable vs immutable: różnice i zastosowania

JAVA 25 SELF
Poziom 34 , Lekcja 3
Dostępny

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.

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