1. Введение
Классические коллекции: гибкость и ловушки
Когда вы создаёте коллекцию с помощью new ArrayList<>(), вы получаете структуру, которую можно свободно изменять: добавлять, удалять, менять элементы. Это удобно, когда вы строите данные «на лету». Но что, если вы передаёте эту коллекцию в другой класс или метод, где её не должны менять? А если вы случайно передадите эту коллекцию наружу, и кто-то её изменит? Вот тут и начинаются проблемы.
Пример классической ошибки
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");
// Передаём коллекцию "вовне"
processNames(names);
// Ожидаем, что список не изменился...
System.out.println(names);
}
public static void processNames(List<String> list) {
// А кто-то взял и удалил элемент!
list.remove("Bob");
}
}
Вывод:
[Alice, Charlie]
Ваша коллекция изменилась, хотя вы этого совершенно не планировали. В больших проектах такие «сюрпризы» легко превращаются в крайне неприятные и трудноуловимые баги.
Опасность в том, что код, работающий с коллекцией, может внезапно столкнуться с непредсказуемыми изменениями данных. К тому же всегда есть риск потери информации — кто-то случайно удалил элемент или переписал его. А если коллекцию одновременно меняют из разных потоков, можно нарваться не только на ConcurrentModificationException, но и на ещё более коварную проблему — неконсистентные данные.
Защита коллекций: старый подход
До Java 9 приходилось использовать методы-обёртки вроде Collections.unmodifiableList(...), о которых мы говорили на прошлом уровне. Они помогают вернуть «замороженную» коллекцию. Но этот способ не всегда удобен и не решает всех проблем (о нём подробнее — в следующей лекции).
2. Современное решение: фабричные методы List.of, Set.of, Map.of
В Java 9 появились новые статические методы в интерфейсах коллекций: List.of, Set.of, Map.of. Они позволяют быстро и удобно создать коллекцию, которую нельзя изменить. Это как если бы вы сделали коллекцию и сразу залили её в бетон — никто не сможет добавить, удалить или изменить элементы.
Пример создания неизменяемых коллекций
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);
}
}
Вывод:
[Alice, Bob, Charlie]
[1, 2, 3]
{Alice=30, Bob=25, Charlie=28}
Как это работает?
- List.of(...) — создаёт неизменяемый список.
- Set.of(...) — создаёт неизменяемое множество.
- Map.of(...) — создаёт неизменяемую карту (до 10 пар ключ-значение; для большего числа используйте Map.ofEntries(...)).
Внимание! Коллекции, созданные этими методами, не допускают изменений. Любая попытка добавить, удалить или заменить элемент приведёт к выбросу исключения.
3. Примеры использования и «подводные камни»
Пример: попытка изменить коллекцию
import java.util.*;
public class ImmutableFail {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob");
// names.add("Charlie"); // Ошибка на этапе выполнения!
try {
names.add("Charlie");
} catch (UnsupportedOperationException ex) {
System.out.println("Нельзя добавить элемент: " + ex.getClass().getSimpleName());
}
}
}
Вывод:
Нельзя добавить элемент: UnsupportedOperationException
Пример: попытка добавить 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 запрещён: " + ex.getClass().getSimpleName());
}
}
}
Вывод:
Null запрещён: NullPointerException
Пример: дубликаты в 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("Дубликаты запрещены: " + ex.getClass().getSimpleName());
}
}
}
Вывод:
Дубликаты запрещены: IllegalArgumentException
Пример: Map.of с большим количеством пар
import java.util.*;
public class MapOfLarge {
public static void main(String[] args) {
// Map.of поддерживает до 10 пар ключ-значение
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);
// Для большего количества используйте Map.ofEntries
Map<String, Integer> bigMap = Map.ofEntries(
Map.entry("eleven", 11),
Map.entry("twelve", 12),
Map.entry("thirteen", 13)
// ...и так далее
);
System.out.println(bigMap);
}
}
4. Особенности и ограничения неизменяемых коллекций
Нельзя изменять.
Любая попытка добавить, удалить или изменить элемент приведёт к UnsupportedOperationException. Даже методы, которые обычно разрешены (add, remove, set), не работают.
Нельзя использовать null.
Если вы попытаетесь добавить null как элемент списка или множества, или как ключ/значение в карту, получите NullPointerException. Это сделано для безопасности: null-элементы часто приводят к ошибкам в коллекциях.
Не гарантируется конкретная реализация.
Вы не узнаете, какой именно класс лежит под капотом у коллекции, созданной через List.of и др. Не стоит делать instanceof ArrayList или пытаться привести коллекцию к какому-то конкретному типу.
Порядок элементов.
— У List.of порядок элементов сохраняется (как в обычном списке).
— У Set.of порядок не гарантируется (на практике может совпадать с порядком передачи аргументов, но лучше не полагаться).
— У Map.of порядок пар не гарантируется.
Быстродействие.
Коллекции, созданные через фабричные методы, обычно работают быстрее, чем обёртки над изменяемыми коллекциями, потому что не тратят память на лишние возможности.
5. Когда и зачем использовать неизменяемые коллекции
Константные наборы данных
Если у вас есть список, множество или карта, которые не должны меняться во время работы программы, используйте List.of, Set.of, Map.of. Например:
private static final List<String> ROLES = List.of("USER", "ADMIN", "MODERATOR");
Теперь никто не сможет добавить в этот список лишнюю роль.
Возвращение коллекций из методов
Если вы возвращаете коллекцию из метода и не хотите, чтобы её кто-то изменил снаружи:
public List<String> getDefaultNames() {
return List.of("Alice", "Bob", "Charlie");
}
Получатель не сможет испортить ваши данные.
Передача между слоями приложения
Когда вы передаёте коллекции между разными частями программы (например, между слоями Controller и Service в веб-приложении), лучше использовать неизменяемые коллекции, чтобы никто не мог «по-тихому» их изменить.
Безопасность и потокобезопасность
Неизменяемые коллекции по определению потокобезопасны в плане чтения: если их никто не может изменить, то их можно смело использовать из разных потоков без синхронизации.
6. Практические примеры для общего приложения
Допустим, в нашем учебном приложении есть список поддерживаемых команд:
public class Commands {
public static final List<String> SUPPORTED_COMMANDS = List.of(
"help", "exit", "list", "add", "remove"
);
}
Если вы попытаетесь сделать так:
Commands.SUPPORTED_COMMANDS.add("hack_the_system");
Вы получите исключение и не сможете навредить приложению.
Или, например, если у вас есть карта с кодами ошибок:
public class ErrorCodes {
public static final Map<Integer, String> CODES = Map.of(
404, "Not Found",
500, "Internal Server Error",
403, "Forbidden"
);
}
Любая попытка добавить новый код — выбросит исключение.
7. Сравнение подходов создания коллекций
| Способ создания | Можно менять? | Можно null? | Дубликаты? | Потокобезопасность | Пример |
|---|---|---|---|---|---|
|
Да | Да | Да | Нет | |
|
Нет | Нет | Да | Да* | |
|
Нет | Нет | Нет | Да* | |
|
Нет | Нет | Нет | Да* | |
|
Нет | Зависит от исходной | Да | Нет | |
* — потокобезопасность только в плане неизменяемости: если коллекцию никто не меняет, её можно безопасно читать из разных потоков.
8. Типичные ошибки при работе с List.of, Set.of, Map.of
Ошибка №1: попытка изменить коллекцию.
Очень частая ошибка — попытаться добавить или удалить элемент из коллекции, созданной через List.of, Set.of или Map.of. Например, names.add("Dmitry") или ages.remove("Bob"). Это всегда приводит к UnsupportedOperationException на этапе выполнения.
Ошибка №2: попытка добавить null.
Если вы случайно передадите null в любой из методов (например, List.of("Alice", null)), получите NullPointerException. Неизменяемые коллекции Java 9+ не любят null — и это, на самом деле, хорошо.
Ошибка №3: дубликаты элементов в Set.of или Map.of.
Set.of("a", "b", "a") или Map.of("x", 1, "x", 2) приведут к IllegalArgumentException. Множество и карта по определению не могут содержать дубликаты.
Ошибка №4: ожидание конкретной реализации.
Не стоит делать так:
List<String> list = List.of("a", "b");
if (list instanceof ArrayList) {
// ...
} // Это всегда false!
Внутренняя реализация скрыта — не полагайтесь на детали реализации.
Ошибка №5: попытка использовать методы изменения коллекции.
Даже такие методы, как clear(), set(index, value) (у списка), будут выбрасывать исключения. Помните: коллекции, созданные через фабричные методы, неизменяемы.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ