JavaRush /Курси /JAVA 25 SELF /Mutable vs immutable колекції: відмінності та застосуванн...

Mutable vs immutable колекції: відмінності та застосування

JAVA 25 SELF
Рівень 34 , Лекція 3
Відкрита

1. Вступ

Якщо сказати просто, mutable-колекція — це та, яку можна змінювати після створення: додавати, видаляти й змінювати елементи. immutable (незмінна) колекція — це колекція, яку після створення змінити не можна. Ніби бетон після застигання: можна дивитися, можна торкатися, але ліпити нові фігурки вже не вийде.

mutable-колекція — це блокнот із олівцем: пишете, стираєте, додаєте нові нотатки. immutable-колекція — це сторінка, яку ви заламінували: тепер ніхто не зможе нічого додати чи стерти.

Приклади змінюваних (mutable) колекцій

У Java майже всі стандартні колекції за замовчуванням змінювані. Це такі класи, як:

  • ArrayList
  • LinkedList
  • HashSet
  • TreeSet
  • HashMap
  • LinkedHashMap
  • та багато інших

Приклад: ArrayList

import java.util.*;

List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.set(1, "Charlie"); // замінили Bob на Charlie
names.remove("Alice");   // видалили Alice
System.out.println(names); // [Charlie]

Тут ми можемо робити з колекцією що завгодно: додавати, видаляти, змінювати елементи місцями. Це зручно, коли колекція будується динамічно, наприклад, у процесі читання даних із файлу або під час користувацького введення.

2. Приклади незмінних (immutable) колекцій

Ці можливості зʼявилися в Java 9. Ймовірно, ви вже з ними знайомі, проте до них ще потрібно звикнути:

  • List.of(...)
  • Set.of(...)
  • Map.of(...)
  • List.copyOf(collection)
  • Set.copyOf(collection)
  • Map.copyOf(map)

Приклад: List.of

List<String> planets = List.of("Mercury", "Venus", "Earth", "Mars");
System.out.println(planets); // [Mercury, Venus, Earth, Mars]
planets.add("Jupiter"); // Кине UnsupportedOperationException!

Спроба змінити колекцію призводить до винятку під час виконання.

Приклад: Collections.unmodifiableList

List<String> modifiable = new ArrayList<>(List.of("a", "b"));
List<String> unmodifiable = Collections.unmodifiableList(modifiable);
unmodifiable.add("c"); // UnsupportedOperationException!

Але тут є підступ: якщо ви зміните вихідну колекцію, обгортка також зміниться!

modifiable.add("c");
System.out.println(unmodifiable); // [a, b, c] — елемент зʼявився!

3. Основні відмінності між mutable та immutable колекціями

Властивість Mutable (змінювані) Immutable (незмінні)
Можна додати елемент? Так Ні
Можна видалити елемент? Так Ні
Можна змінити елемент? Так (наприклад, set) Ні
Потокобезпечність Ні (за замовчуванням) Так (немає стану — нічого змінювати)
Можна додати null? Так (зазвичай) Ні (у фабричних методах Java 9+)
Реалізація ArrayList, HashSet тощо List.of, Set.of, Map.of, copyOf

4. Навіщо взагалі потрібні незмінні колекції?

Одразу виникає запитання: якщо змінювані колекції такі гнучкі, навіщо потрібні незмінні? Причин насправді багато, і всі вони повʼязані з безпекою, читабельністю та передбачуваністю коду.

Безпека та захист від помилок

Коли ви віддаєте колекцію назовні (наприклад, із методу або класу), ви хочете бути впевнені, що ніхто випадково не змінить її вміст. Особливо це важливо, якщо колекція містить «важливі» дані, які не повинні змінюватися після ініціалізації.

Приклад:

public class Team {
    private final List<String> players;

    public Team(List<String> players) {
        // Робимо незмінну копію, щоб ніхто не міг підмінити склад
        this.players = List.copyOf(players);
    }

    public List<String> getPlayers() {
        return players;
    }
}

Тепер будь-який код, що отримав список гравців, не зможе додати туди свого «друга».

Потокобезпечність

Змінювані колекції небезпечні під час доступу з кількох потоків одночасно. Незмінні колекції, навпаки, можна вільно передавати між потоками — ніхто не зможе їх зіпсувати.

Спрощення налагодження

Якщо колекція не змінюється, ви завжди знаєте, що в ній лежить. Не треба боятися, що хтось її «тихо» змінив в іншому місці коду.

Використання як ключі або значення в інших колекціях

Незмінні обʼєкти — ідеальні кандидати для використання як ключі в Map або елементи в Set. Якщо обʼєкт може змінитися після додавання, ви ризикуєте втратити до нього доступ (див. про hashCode та equals).

5. Коли краще використовувати змінювані колекції?

Змінювані колекції доречні, коли:

  • Колекція будується поетапно, у циклі або з різних джерел.
  • Потрібні часті зміни: додавання, видалення, сортування.
  • Колекція призначена для внутрішнього використання, і ніхто «ззовні» не зможе її зіпсувати.

Приклад: побудова списку

List<String> shoppingList = new ArrayList<>();
shoppingList.add("Молоко");
shoppingList.add("Хліб");
shoppingList.add("Яблука");
// Після побудови — можна зробити незмінну версію
List<String> finalList = List.copyOf(shoppingList);

6. Коли краще використовувати незмінні колекції?

  • Для зберігання константних даних (наприклад, список днів тижня).
  • Для передачі колекцій між шарами застосунку (наприклад, із DAO до сервісу).
  • Для повернення колекцій із методів, щоб захистити їх від змін.
  • Для багатопоточної роботи, де важлива безпека.

Приклад: константні дані

public static final List<String> WEEKDAYS = List.of(
    "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"
);

Приклад: передача назовні

public List<String> getReadOnlyNames() {
    return List.copyOf(names); // ніхто не зможе змінити список
}

7. Особливості та підводні камені

Незмінність ≠ потокобезпечність

Незмінна колекція захищена від змін, але це не означає, що вона захищена від інших видів проблем у багатопотоковому середовищі (наприклад, якщо елементи колекції — змінювані обʼєкти).

List<List<String>> listOfLists = List.of(new ArrayList<>());
listOfLists.get(0).add("Oops!"); // Можна змінити внутрішній список!

Обгортки vs копії

Як ми вже згадували, Collections.unmodifiableList — це обгортка, і якщо вихідну колекцію змінити, обгортка також зміниться. А ось List.copyOf створює справжню незалежну копію.

List<String> base = new ArrayList<>(List.of("a", "b"));
List<String> wrap = Collections.unmodifiableList(base);
List<String> copy = List.copyOf(base);

base.add("c");
System.out.println(wrap); // [a, b, c] — змінилося!
System.out.println(copy); // [a, b] — залишилося незмінним!

NullPointerException

Фабричні методи (List.of, Set.of, Map.of) не дозволяють додавати null:

List<String> bad = List.of("a", null); // Кине NullPointerException вже під час створення!

8. Порівняння підходів: переваги та недоліки

Змінювані колекції

Головна перевага змінюваних колекцій — їхня гнучкість. Ви можете додавати й видаляти елементи прямо на ходу, перебудовувати колекцію в міру розвитку логіки програми. Такий підхід особливо зручний, коли потрібно швидко зібрати тимчасову структуру або щось динамічно змінювати.

Але за зручність доводиться платити. Змінювана колекція завжди несе ризик випадкового втручання: хтось у коді може ненароком змінити дані, і це призведе до важковловимих помилок. У багатопоточних програмах ситуація ще гірша — паралельні зміни легко призводять до гонок даних і неочікуваних збоїв. Контролювати життєвий цикл таких даних також складніше: потрібно постійно памʼятати, хто й коли може змінити колекцію.

Незмінні колекції

З незмінними колекціями працювати спокійніше. Ви точно знаєте: ніхто не підправить їх «за спиною». Це робить код безпечнішим, його простіше налагоджувати й тестувати, а самі колекції зручно передавати між потоками без додаткових блокувань.

Недолік у тому, що іноді доводиться жертвувати продуктивністю. Якщо потрібно додати новий елемент, доводиться створювати нову копію колекції, що тягне за собою додаткові витрати памʼяті та часу. Крім того, збирати великі й складні структури одразу в незмінному вигляді буває незручно. Зазвичай їх будують у тимчасовій змінюваній колекції, а коли все готово — «заморожують».

9. Типові помилки

Помилка № 1: Повернення змінюваної колекції назовні. Якщо ви повертаєте з методу звичайний ArrayList, будь-який зовнішній код може додати або видалити елементи. Це може призвести до помилок, які дуже складно відстежити.

Помилка № 2: Використання обгортки замість копії. Якщо ви використовуєте Collections.unmodifiableList, але вихідна колекція десь іще змінюється, «незмінність» — ілюзія.

Помилка № 3: Змінювані обʼєкти всередині незмінних колекцій. Навіть якщо сама колекція незмінна, її елементи можуть бути змінюваними. Це може призвести до неочікуваних змін стану.

Помилка № 4: Спроба додати null у колекцію, створену через фабричні методи. На відміну від старих колекцій, нові фабричні методи не дозволяють додавати null — отримаєте NullPointerException.

Помилка № 5: Очікування конкретної реалізації. Колекції, створені через List.of або Set.of, не гарантують тип реалізації (це не обовʼязково ArrayList або HashSet). Не покладайтеся на це.

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