1. Проблема изменяемости коллекций
В Java коллекции напоминают склад с товарами: любой может прийти и что-то добавить, убрать, поменять. Иногда это удобно, но в больших программах это превращается в головную боль. Представьте, что вы передали список товаров из своего класса наружу, а кто-то взял и удалил половину позиций. Или — что ещё веселее — в многопоточной программе один поток добавляет элементы, а другой их читает: результат может быть неожиданным, а ошибки — трудноуловимыми (например, ConcurrentModificationException).
Вот пример, почему изменяемость коллекций — источник багов:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("Чай");
products.add("Кофе");
}
public List<String> getProducts() {
// ОПАСНО! Возвращаем ссылку на внутренний список
return products;
}
}
public class Main {
public static void main(String[] args) {
Inventory inv = new Inventory();
List<String> external = inv.getProducts();
external.remove("Чай"); // Опа! Теперь в инвентаре нет чая
System.out.println(inv.getProducts()); // [Кофе]
}
}
Заметили подвох? Один метод возвращает внутреннюю коллекцию, другой её меняет. Так можно случайно уничтожить данные, которые должны быть защищены.
2. Создание неизменяемых коллекций: Collections.unmodifiable*
Чтобы избежать подобных конфузов, Java предлагает эффективную защиту: коллекцию можно сделать «неизменяемой» с помощью специальных обёрток из класса Collections:
- Collections.unmodifiableList(list)
- Collections.unmodifiableSet(set)
- Collections.unmodifiableMap(map)
Как это работает? Сначала вы создаёте обычную коллекцию, а потом оборачиваете её в «неизменяемую» оболочку:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("Чай");
drinks.add("Кофе");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
System.out.println(immutableDrinks); // [Чай, Кофе]
// Попробуем добавить элемент
immutableDrinks.add("Какао"); // Бах! UnsupportedOperationException
}
}
Попытка изменить такую коллекцию приводит к выбрасыванию исключения UnsupportedOperationException. Это как если бы вы налепили на коробку огромную наклейку "НЕ ТРОГАТЬ!" — и каждый, кто попытается добавить или удалить что-то, получит по рукам (или по стеку вызовов).
Пример: защищаем внутреннее состояние
Давайте исправим наш класс Inventory из прошлого примера:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("Чай");
products.add("Кофе");
}
public List<String> getProducts() {
// Теперь возвращаем обёртку
return Collections.unmodifiableList(products);
}
}
Теперь, если кто-то попробует изменить полученный список, он получит исключение.
3. Поведение неизменяемых коллекций: поверхностная защита
Важно понимать: unmodifiableList и его «собратья» делают только оболочку вокруг исходной коллекции. Они не создают копию — любые изменения исходной коллекции (той, что «внутри») будут видны и в обёртке!
Демонстрация
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("Чай");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
drinks.add("Кофе"); // Меняем исходную коллекцию
System.out.println(immutableDrinks); // [Чай, Кофе] — элемент появился!
}
}
Вывод: обёртка защищает только от изменений через саму обёртку. Если кто-то держит ссылку на исходную коллекцию, он всё равно может её менять.
4. Глубокая неизменяемость: мифы и реальность
Обёртки unmodifiable* делают коллекцию неизменяемой только снаружи. Но если коллекция содержит изменяемые объекты, их можно менять!
Пример
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("Чай"));
List<Product> immutableProducts = Collections.unmodifiableList(products);
// Меняем объект внутри коллекции
immutableProducts.get(0).name = "Кофе";
System.out.println(immutableProducts); // [Кофе]
}
}
Вывод:
- Коллекция «неизменяемая», но объекты внутри — нет.
- Для полной (глубокой) неизменяемости используйте неизменяемые объекты (например, String, Integer, record-классы или делайте свои классы immutable).
5. Когда использовать неизменяемые коллекции
Для защиты внутреннего состояния
Если вы пишете класс, который хранит коллекцию, и отдаёте её наружу, всегда возвращайте обёртку, чтобы никто не мог случайно (или специально) изменить ваши данные:
public List<String> getProducts() {
return Collections.unmodifiableList(products);
}
В многопоточных программах
В многопоточных приложениях изменяемые коллекции — источник проблем (race condition, ConcurrentModificationException и прочие «радости жизни»). Если коллекцию не нужно менять после создания — делайте её неизменяемой.
Для передачи данных между слоями
Если вы передаёте коллекцию из одного слоя программы в другой (например, из DAO в сервис), передавайте неизменяемую копию или обёртку — это защищает от случайных изменений.
6. Практические примеры
Пример 1: защищаем список студентов
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);
}
}
Теперь никто не сможет добавить или удалить студента напрямую через getStudents().
Пример 2: неизменяемая карта (Map)
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> grades = new HashMap<>();
grades.put("Вася", 5);
grades.put("Маша", 4);
Map<String, Integer> immutableGrades = Collections.unmodifiableMap(grades);
// immutableGrades.put("Петя", 3); // UnsupportedOperationException
}
}
7. Полезные нюансы
Современные альтернативы: List.of, Set.of, Map.of
В Java 9 появились ещё более удобные способы создания неизменяемых коллекций:
List<String> drinks = List.of("Чай", "Кофе");
Set<String> fruits = Set.of("Яблоко", "Банан");
Map<String, Integer> ages = Map.of("Вася", 20, "Маша", 21);
- Эти коллекции неизменяемы (любая попытка изменить — исключение).
- Они не имеют «исходной» изменяемой коллекции (в отличие от Collections.unmodifiable*).
- Не допускают значения null.
Также с Java 10 есть методы-копии: List.copyOf, Set.copyOf, Map.copyOf — создают неизменяемую копию переданной коллекции.
Сравнение способов создания неизменяемых коллекций
| Способ | Глубокая неизменяемость | Можно ли менять исходную коллекцию? | Допускает null? | Версия Java |
|---|---|---|---|---|
|
Нет | Да | Да | 1.2 |
|
Нет | Нет (нет исходной коллекции) | Нет | 9+ |
8. Типичные ошибки при работе с неизменяемыми коллекциями
Ошибка №1: Изменение исходной коллекции после создания обёртки. Вы создали unmodifiableList, а потом кто-то меняет исходный список. Обёртка не защитит от этого — изменения будут видны во всех местах, где используется обёртка.
Ошибка №2: Ожидание глубокой неизменяемости. Многие думают, что если коллекция неизменяема, то и объекты внутри неё тоже нельзя менять. На деле защищается только структура (добавление/удаление/изменение через коллекцию), но не содержимое объектов.
Ошибка №3: Использование неизменяемых коллекций с значением null в современных фабриках. Коллекции, созданные через List.of, Set.of, Map.of, не допускают null. Попытка добавить или получить null приведёт к исключению.
Ошибка №4: Передача ссылок на изменяемые коллекции наружу. Если вы возвращаете наружу ссылку на внутреннюю коллекцию (без обёртки), вы теряете контроль над своими данными — прямой путь к багам и «протечке» инвариантов.
Ошибка №5: Использование неизменяемых коллекций в коде, который ожидает изменяемость. Если сторонний код попытается изменить коллекцию (например, добавить элемент), он получит UnsupportedOperationException. Убедитесь, что потребители знают о неизменяемости данных.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ