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. Переконайтеся, що споживачі знають про незмінність даних.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ