JavaRush /Курсы /JAVA 25 SELF /Неизменяемые коллекции: Collections.unmodifiable

Неизменяемые коллекции: Collections.unmodifiable

JAVA 25 SELF
33 уровень , 2 лекция
Открыта

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
Collections.unmodifiableList(list)
Нет Да Да 1.2
List.of(...), Set.of(...), Map.of(...)
Нет Нет (нет исходной коллекции) Нет 9+

8. Типичные ошибки при работе с неизменяемыми коллекциями

Ошибка №1: Изменение исходной коллекции после создания обёртки. Вы создали unmodifiableList, а потом кто-то меняет исходный список. Обёртка не защитит от этого — изменения будут видны во всех местах, где используется обёртка.

Ошибка №2: Ожидание глубокой неизменяемости. Многие думают, что если коллекция неизменяема, то и объекты внутри неё тоже нельзя менять. На деле защищается только структура (добавление/удаление/изменение через коллекцию), но не содержимое объектов.

Ошибка №3: Использование неизменяемых коллекций с значением null в современных фабриках. Коллекции, созданные через List.of, Set.of, Map.of, не допускают null. Попытка добавить или получить null приведёт к исключению.

Ошибка №4: Передача ссылок на изменяемые коллекции наружу. Если вы возвращаете наружу ссылку на внутреннюю коллекцию (без обёртки), вы теряете контроль над своими данными — прямой путь к багам и «протечке» инвариантов.

Ошибка №5: Использование неизменяемых коллекций в коде, который ожидает изменяемость. Если сторонний код попытается изменить коллекцию (например, добавить элемент), он получит UnsupportedOperationException. Убедитесь, что потребители знают о неизменяемости данных.

1
Задача
JAVA 25 SELF, 33 уровень, 2 лекция
Недоступна
Создание защищенного списка редких книг в библиотеке 📚
Создание защищенного списка редких книг в библиотеке 📚
1
Задача
JAVA 25 SELF, 33 уровень, 2 лекция
Недоступна
Динамическое меню в ресторане: как изменения в рецептах влияют на гостевую версию 🍽️
Динамическое меню в ресторане: как изменения в рецептах влияют на гостевую версию 🍽️
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Александр Уровень 50
12 декабря 2025
В Collections есть вложенный класс

static class UnmodifiableList<E> extends UnmodifiableCollection<E> implements List<E>
При создании объекта типа UnmodifiableList, он хранит ссылку на исходный список, которую получает при вызове конструктора:

final List<? extends E> list;
        UnmodifiableList(List<? extends E> list) {
            super(list);
            this.list = list;}
И по этой ссылке прекрасно работает с исходным списком

public E get(int index) {return list.get(index);}
Но ограничен в методах. Методы на изменение и запись бросают исключения:

public void add(int index, E element) {
            throw new UnsupportedOperationException();
        }
        public E remove(int index) {
            throw new UnsupportedOperationException();
        }
Тот случай, когда посмотреть код в Идее оказалось нагляднее, быстрее и понятнее, чем лекция. Заодно и пример класса-обертки увидели. Может кому-то понадобится.