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. Переконайтеся, що споживачі знають про незмінність даних.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ