JavaRush /Курси /JAVA 25 SELF /List.of, Set.of, Map.of — незмінні колекції

List.of, Set.of, Map.of — незмінні колекції

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

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? Дублікати? Потокобезпечність Приклад
new ArrayList<>()
Так Так Так Ні
new ArrayList<>()
List.of(...)
Ні Ні Так Так*
List.of("a", "b")
Set.of(...)
Ні Ні Ні Так*
Set.of("a", "b")
Map.of(...)
Ні Ні Ні Так*
Map.of("a", 1, "b", 2)
Collections.unmodifiableList(...)
Ні Залежить від вихідної колекції Так Ні
Collections.unmodifiableList(list)

* — потокобезпечність лише у сенсі незмінності: якщо колекцію ніхто не змінює, її можна безпечно читати з різних потоків.

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) (у списку), викидатимуть винятки. Памʼятайте: колекції, створені через фабричні методи, незмінні.

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