1. Wprowadzenie
Klasyczne kolekcje: elastyczność i pułapki
Kiedy tworzysz kolekcję za pomocą new ArrayList<>(), otrzymujesz strukturę, którą można swobodnie modyfikować: dodawać, usuwać, zmieniać elementy. To wygodne, gdy budujesz dane „w locie”. Ale co, jeśli przekazujesz tę kolekcję do innej klasy lub metody, gdzie nie powinna być zmieniana? A jeśli przypadkowo udostępnisz tę kolekcję na zewnątrz i ktoś ją zmodyfikuje? Właśnie tu zaczynają się problemy.
Przykład klasycznego błędu
import java.util.*;
public class Example {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// Przekazujemy kolekcję "na zewnątrz"
processNames(names);
// Zakładamy, że lista się nie zmieniła...
System.out.println(names);
}
public static void processNames(List<String> list) {
// A ktoś po prostu usunął element!
list.remove("Bob");
}
}
Wynik:
[Alice, Charlie]
Twoja kolekcja się zmieniła, choć wcale tego nie planowałeś. W dużych projektach takie „niespodzianki” łatwo przeradzają się w bardzo nieprzyjemne i trudne do wychwycenia błędy.
Niebezpieczeństwo polega na tym, że kod pracujący z kolekcją może nagle zetknąć się z nieprzewidywalnymi zmianami danych. Do tego zawsze istnieje ryzyko utraty informacji — ktoś przypadkowo usunął element lub go nadpisał. A jeśli kolekcję jednocześnie modyfikują różne wątki, można natknąć się nie tylko na ConcurrentModificationException, ale i na jeszcze bardziej podstępny problem — niespójne dane.
Ochrona kolekcji: stare podejście
Przed Java 9 trzeba było używać metod-owijek w rodzaju Collections.unmodifiableList(...), o których mówiliśmy na poprzednim poziomie. Pomagają one zwrócić „zamrożoną” kolekcję. Jednak ten sposób nie zawsze jest wygodny i nie rozwiązuje wszystkich problemów (o nim szerzej — w następnej lekcji).
2. Nowe rozwiązanie: metody fabryczne List.of, Set.of, Map.of
W Java 9 pojawiły się nowe metody statyczne w interfejsach kolekcji: List.of, Set.of, Map.of. Pozwalają one szybko i wygodnie utworzyć kolekcję, której nie można zmienić. To tak, jakbyś stworzył kolekcję i od razu zalał ją betonem — nikt nie będzie mógł dodać, usunąć ani zmienić elementów.
Przykład tworzenia niemutowalnych kolekcji
import java.util.*;
public class ImmutableDemo {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
Set<Integer> numbers = Set.of(1, 2, 3);
Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25, "Charlie", 28);
System.out.println(names);
System.out.println(numbers);
System.out.println(ages);
}
}
Wynik:
[Alice, Bob, Charlie]
[1, 2, 3]
{Alice=30, Bob=25, Charlie=28}
Jak to działa?
- List.of(...) — tworzy niemutowalną listę.
- Set.of(...) — tworzy niemutowalny zbiór.
- Map.of(...) — tworzy niemutowalną mapę (do 10 par klucz-wartość; dla większej liczby użyj Map.ofEntries(...)).
Uwaga! Kolekcji utworzonych tymi metodami nie można modyfikować. Każda próba dodania, usunięcia lub zastąpienia elementu spowoduje wyrzucenie wyjątku.
3. Przykłady użycia i „pułapki”
Przykład: próba zmiany kolekcji
import java.util.*;
public class ImmutableFail {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob");
// names.add("Charlie"); // Błąd w czasie wykonania!
try {
names.add("Charlie");
} catch (UnsupportedOperationException ex) {
System.out.println("Nie można dodać elementu: " + ex.getClass().getSimpleName());
}
}
}
Wynik:
Nie można dodać elementu: UnsupportedOperationException
Przykład: próba dodania null
import java.util.*;
public class NullFail {
public static void main(String[] args) {
try {
List<String> badList = List.of("Alice", null, "Bob");
} catch (NullPointerException ex) {
System.out.println("Null niedozwolony: " + ex.getClass().getSimpleName());
}
}
}
Wynik:
Null niedozwolony: NullPointerException
Przykład: duplikaty w Set.of
import java.util.*;
public class DuplicatesFail {
public static void main(String[] args) {
try {
Set<String> badSet = Set.of("one", "two", "one");
} catch (IllegalArgumentException ex) {
System.out.println("Duplikaty niedozwolone: " + ex.getClass().getSimpleName());
}
}
}
Wynik:
Duplikaty niedozwolone: IllegalArgumentException
Przykład: Map.of z dużą liczbą par
import java.util.*;
public class MapOfLarge {
public static void main(String[] args) {
// Map.of obsługuje do 10 par klucz-wartość
Map<String, Integer> map = Map.of(
"one", 1, "two", 2, "three", 3, "four", 4, "five", 5,
"six", 6, "seven", 7, "eight", 8, "nine", 9, "ten", 10
);
System.out.println(map);
// Dla większej liczby użyj Map.ofEntries
Map<String, Integer> bigMap = Map.ofEntries(
Map.entry("eleven", 11),
Map.entry("twelve", 12),
Map.entry("thirteen", 13)
// ...i tak dalej
);
System.out.println(bigMap);
}
}
4. Cechy i ograniczenia niemutowalnych kolekcji
Nie można modyfikować.
Każda próba dodania, usunięcia lub zmiany elementu spowoduje UnsupportedOperationException. Nawet metody, które zwykle są dozwolone (add, remove, set), nie działają.
Nie można używać null.
Jeśli spróbujesz dodać null jako element listy lub zbioru albo jako klucz/wartość do mapy, otrzymasz NullPointerException. Zrobiono to dla bezpieczeństwa: elementy null często prowadzą do błędów w kolekcjach.
Nie jest gwarantowana konkretna implementacja.
Nie dowiesz się, jaka dokładnie klasa kryje się pod spodem kolekcji utworzonej przez List.of i inne. Nie rób instanceof ArrayList ani nie próbuj rzutować kolekcji na jakiś konkretny typ.
Kolejność elementów.
— W List.of kolejność elementów jest zachowana (jak w zwykłej liście).
— W Set.of kolejność nie jest gwarantowana (w praktyce może odpowiadać kolejności przekazywania argumentów, ale lepiej na to nie liczyć).
— W Map.of kolejność par nie jest gwarantowana.
Wydajność.
Kolekcje utworzone przez metody fabryczne zwykle działają szybciej niż owijki nad kolekcjami modyfikowalnymi, ponieważ nie zużywają pamięci na zbędne możliwości.
5. Kiedy i po co używać niemutowalnych kolekcji
Stałe zbiory danych
Jeśli masz listę, zbiór lub mapę, które nie powinny się zmieniać podczas działania programu, użyj List.of, Set.of, Map.of. Na przykład:
private static final List<String> ROLES = List.of("USER", "ADMIN", "MODERATOR");
Teraz nikt nie doda do tej listy zbędnej roli.
Zwracanie kolekcji z metod
Jeśli zwracasz kolekcję z metody i nie chcesz, by ktoś ją zmienił z zewnątrz:
public List<String> getDefaultNames() {
return List.of("Alice", "Bob", "Charlie");
}
Odbiorca nie będzie mógł popsuć twoich danych.
Przekazywanie między warstwami aplikacji
Kiedy przekazujesz kolekcje między różnymi częściami programu (np. między warstwami Controller i Service w aplikacji webowej), lepiej używać niemutowalnych kolekcji, aby nikt nie mógł ich „po cichu” zmienić.
Bezpieczeństwo i bezpieczeństwo wątkowe
Niemutowalne kolekcje z definicji są bezpieczne w kontekście odczytu: skoro nikt nie może ich zmienić, można ich bezpiecznie używać z wielu wątków bez synchronizacji.
6. Praktyczne przykłady dla typowej aplikacji
Załóżmy, że w naszej aplikacji edukacyjnej jest lista wspieranych poleceń:
public class Commands {
public static final List<String> SUPPORTED_COMMANDS = List.of(
"help", "exit", "list", "add", "remove"
);
}
Jeśli spróbujesz zrobić coś takiego:
Commands.SUPPORTED_COMMANDS.add("hack_the_system");
Otrzymasz wyjątek i nie zaszkodzisz aplikacji.
Albo na przykład, jeśli masz mapę z kodami błędów:
public class ErrorCodes {
public static final Map<Integer, String> CODES = Map.of(
404, "Not Found",
500, "Internal Server Error",
403, "Forbidden"
);
}
Każda próba dodania nowego kodu — skończy się wyjątkiem.
7. Porównanie podejść tworzenia kolekcji
| Sposób tworzenia | Można zmieniać? | Null dozwolony? | Duplikaty? | Bezpieczeństwo wątkowe | Przykład |
|---|---|---|---|---|---|
|
Tak | Tak | Tak | Nie | |
|
Nie | Nie | Tak | Tak* | |
|
Nie | Nie | Nie | Tak* | |
|
Nie | Nie | Nie | Tak* | |
|
Nie | Zależy od kolekcji źródłowej | Tak | Nie | |
* — bezpieczeństwo wątkowe tylko w sensie niemutowalności: jeśli nikt nie zmienia kolekcji, można ją bezpiecznie czytać z wielu wątków.
8. Typowe błędy przy pracy z List.of, Set.of, Map.of
Błąd nr 1: próba zmiany kolekcji.
Bardzo częsty błąd — próba dodania lub usunięcia elementu z kolekcji utworzonej przez List.of, Set.of lub Map.of. Na przykład, names.add("Dmitry") lub ages.remove("Bob"). To zawsze prowadzi do UnsupportedOperationException w czasie wykonania.
Błąd nr 2: próba dodania null.
Jeśli przypadkiem przekażesz null do którejkolwiek z metod (np. List.of("Alice", null)), otrzymasz NullPointerException. Niemutowalne kolekcje Java 9+ nie lubią null — i to, tak naprawdę, dobrze.
Błąd nr 3: duplikaty elementów w Set.of lub Map.of.
Set.of("a", "b", "a") lub Map.of("x", 1, "x", 2) spowodują IllegalArgumentException. Zbiór i mapa z definicji nie mogą zawierać duplikatów.
Błąd nr 4: oczekiwanie konkretnej implementacji.
Nie rób tak:
List<String> list = List.of("a", "b");
if (list instanceof ArrayList) {
// ...
} // To zawsze false!
Wewnętrzna implementacja jest ukryta — nie polegaj na szczegółach implementacyjnych.
Błąd nr 5: próba użycia metod modyfikujących kolekcję.
Nawet takie metody jak clear(), set(index, value) (dla listy) będą wyrzucać wyjątki. Pamiętaj: kolekcje tworzone metodami fabrycznymi są niemutowalne.
GO TO FULL VERSION