Що таке Collections.unmodifiableList() і як це працює
Collections.unmodifiableList() не виділяє пам'ять під новий масив елементів і не копіює дані:
🟢 Junior Level
30-секундна відповідь
Collections.unmodifiableList() — це статичний фабричний метод стандартної бібліотеки Java (існує ще з Java 1.2), який обгортає наявний список у спеціальне немодифіковне представлення (Read-Only View).
- Будь-яка спроба змінити колекцію через це представлення (виклики методів
add(),remove(),clear(),set(),replaceAll(),sort()) негайно завершується виняткомUnsupportedOperationException. - Методи читання (
get(),size(),contains(),iterator()) працюють у штатному режимі, прозоро делегуючи виклики оригінальному списку.
Критична пастка: Це ОБГОРТКА, а НЕ копія!
Collections.unmodifiableList() не виділяє пам’ять під новий масив елементів і не копіює дані:
- Він лише зберігає пряме посилання на вихідний список.
- Якщо оригінальний список буде змінено в іншому місці програми, усі ці зміни миттєво стануть видимими через unmodifiable-обгортку!
List<String> original = new ArrayList<>();
original.add("Java");
List<String> view = Collections.unmodifiableList(original);
// Спроба прямої модифікації через обгортку:
view.add("Kotlin"); // ❌ ВИНЯТОК: UnsupportedOperationException!
// Модифікація оригінального списку:
original.add("Kotlin"); // ✅ Успішно!
// Зміна негайно відобразилася в "незмінній" обгортці:
System.out.println(view); // Надрукує: [Java, Kotlin]
Наочна аналогія з життя
Уявіть музейну вітрину з броньованим склом:
- Відвідувач музею дивиться на експонат крізь скло (
unmodifiableList) і не може до нього доторкнутися чи забрати. - Проте співробітник музею з ключем зі зворотного боку (
original) може в будь-який момент прибрати експонат або замінити його на інший.
🟡 Middle Level
Патерн проєктування Decorator (Декоратор)
Всередині метод Collections.unmodifiableList() повертає екземпляр приватного статичного класу Collections.UnmodifiableList:
static class UnmodifiableList<E> extends UnmodifiableCollection<E> implements List<E> {
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 int size() { return list.size(); }
public boolean contains(Object o) { return list.contains(o); }
// Перехоплення та блокування всіх методів-мутаторів:
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
public void clear() {
throw new UnsupportedOperationException();
}
}
Обгортання ітераторів
Методи iterator() та listIterator() повертають спеціальні реалізації ітераторів, які делегують методи hasNext() та next() вихідному ітератору, але блокують метод remove():
public Iterator<E> iterator() {
return new Iterator<E>() {
private final Iterator<? extends E> i = c.iterator();
public boolean hasNext() { return i.hasNext(); }
public E next() { return i.next(); }
public void remove() {
throw new UnsupportedOperationException(); // Блокування!
}
};
}
Маркерний інтерфейс RandomAccess
Якщо переданий список реалізує інтерфейс швидкої індексної адресації RandomAccess (наприклад, ArrayList), метод Collections.unmodifiableList() повертає спеціалізований підклас UnmodifiableRandomAccessList:
public static <T> List<T> unmodifiableList(List<? extends T> list) {
return (list instanceof RandomAccess ?
new UnmodifiableRandomAccessList<>(list) :
new UnmodifiableList<>(list));
}
Це зберігає $O(1)$ продуктивність алгоритмів (таких як Collections.binarySearch), що перевіряють умову if (list instanceof RandomAccess).
🔴 Senior Level
Багатопотокові ризики: Ілюзія потокобезпечності
Серед розробників поширена небезпечна омана: «Якщо список немодифіковний, його можна безпечно віддавати на читання в паралельні потоки». Це не так!
ConcurrentModificationExceptionпри паралельній ітерації:- Якщо Потік 1 ітерується колекцією через
unmodifiableList, а Потік 2 в цей час викликаєoriginalList.add(), лічильник модифікаційmodCountбазовогоArrayListзбільшується. - Ітератор Потоку 1 миттєво падає з
ConcurrentModificationExceptionпрямо під час викликуnext().
- Якщо Потік 1 ітерується колекцією через
- Відсутність бар’єрів пам’яті (Safe Publication):
- Клас
UnmodifiableListне використовує ключові словаvolatileчи бар’єри пам’яті для елементів вихідного списку. - Потік-читач може побачити елементи в стані розірваного читання або зі застарілими закешованими даними в кешах L1/L2 процесора.
- Клас
Серіалізація та десеріалізація
Клас UnmodifiableList реалізує інтерфейс java.io.Serializable:
- При серіалізації записується сам об’єкт-обгортка та посилання на оригінальний список.
- При десеріалізації відновлюється як оригінальний список, так і
UnmodifiableList, що його загортає. - Важливо: якщо вихідний список не реалізує
Serializable, спроба серіалізуватиunmodifiableListзавершиться виняткомNotSerializableException!
Контракт equals() та hashCode()
Цікава архітектурна деталь JDK:
- Базовий клас
UnmodifiableCollectionнавмисно не перевизначаєequals()таhashCode(), успадковуючи їх відObject(порівняння за посиланням==). Це зроблено тому, що контракти рівності дляListтаSetє кардинально різними (дляListважливий порядок елементів, дляSet— лише їх наявність). - Підклас
UnmodifiableListявно перевизначаєequals()таhashCode(), суворо делегуючи їх оригінальному спискуlist.equals(o)таlist.hashCode(), повністю дотримуючись контракту інтерфейсуjava.util.List.
4 Tricky Questions
1. Чому Collections.unmodifiableCollection() навмисно НЕ перевизначає equals() та hashCode(), а Collections.unmodifiableList() — перевизначає?
Відповідь:
Інтерфейс java.util.Collection свідомо не фіксує контракт для equals() та hashCode(), залишаючи його відкритим, оскільки підтипи List та Set мають взаємовиключні правила:
List.equals()вимагає точного збігу елементів та їхніх порядкових позицій.Set.equals()вимагає збігу елементів незалежно від порядку обходу. ЯкбиUnmodifiableCollectionреалізував одне з цих правил, це зламало б поліморфізм для іншого підтипу. ТомуUnmodifiableCollectionзалишає реалізаціюObject, а спеціалізовані класиUnmodifiableListтаUnmodifiableSetреалізують відповідний інтерфейсний контракт, делегуючи виклики внутрішньому списку або сету.
2. Чи скомпілюється виклик Collections.sort(unmodifiableList), і що станеться під час виконання? А виклик unmodifiableList.sort(null)?
Відповідь:
Обидва виклики успішно скомпілюються, оскільки метод Collections.sort(List<T>) приймає будь-який об’єкт, що реалізує інтерфейс List.
Однак під час виконання:
unmodifiableList.sort(...): класUnmodifiableListявно перевизначає метод за замовчуваннямsort(Comparator)інтерфейсуList(який з’явився в Java 8) і викидаєnew UnsupportedOperationException().Collections.sort(unmodifiableList)всередині себе викликає методlist.sort(), який потрапляє на ту саму реалізацію і негайно викидаєUnsupportedOperationException.
3. Чим поведінка UnmodifiableRandomAccessList відрізняється від UnmodifiableList, і чому це критично для продуктивності?
Відповідь:
Єдина відмінність UnmodifiableRandomAccessList від UnmodifiableList — наявність порожнього маркерного інтерфейсу implements RandomAccess.
Проте це критично для швидкодії стандартних алгоритмів JDK:
- Такі методи, як
Collections.binarySearch(), перевіряють:if (list instanceof RandomAccess || list.size() < BINARYSEARCH_THRESHOLD) return indexedBinarySearch(list, key); // O(log N) прямого індексного доступу else return iteratorBinarySearch(list, key); // O(N) послідовного доступу через ітератор - Якби обгортка втрачала маркер
RandomAccess, бінарний пошук по загорнутомуArrayListпочав би працювати за $O(N)$ замість $O(\log N)$, спричиняючи колосальні просідання швидкодії.
4. В якому єдиному сценарії використання Collections.unmodifiableList() є кращим, ніж List.copyOf() у сучасній Java?
Відповідь:
Collections.unmodifiableList() є кращим у двох випадках:
- Екстремальна економія пам’яті та часу ($O(1)$ без алокації масиву): якщо колекція містить сотні тисяч елементів, уже надійно інкапсульована всередині сервісу й гарантовано ніколи не модифікується, створення обгортки за $O(1)$ без виділення десятків мегабайтів під копію масиву є цілком виправданим.
- Наявність елементів
null: якщо за контрактом бізнес-логіки в колекції допустимі елементиnull, викликList.copyOf()викинеNullPointerException, тоді якCollections.unmodifiableList()коректно збереже і дозволить безпечно читати список ізnull-елементами.
🎯 Шпаргалка для інтерв’ю
Головне про UnmodifiableList
- Патерн: Decorator (View).
- Складність: $O(1)$ за часом створення та додатковою пам’яттю.
- Обмеження: забороняє операції запису через себе, але не захищає від мутацій базового списку.
- Багатопотоковість: НЕ є потокобезпечним. Паралельна зміна оригінального списку призведе до
ConcurrentModificationException. - Маркер RandomAccess: зберігається для швидкого індексного пошуку.
Порівняння: UnmodifiableList vs List.copyOf
| Характеристика | Collections.unmodifiableList(orig) | List.copyOf(orig) |
| :— | :— | :— |
| Принцип роботи | Обгортка (View) поверх оригіналу | Повноцінна захисна копія |
| Складність створення | $O(1)$ | $O(N)$ (або $O(1)$ якщо вже immutable) |
| Ізоляція від змін | ❌ Ні (пов’язаний з оригіналом) | ✅ Повна ізоляція |
| Ставлення до null | Дозволяє null | Забороняє null (викидає NPE) |
Червоні прапорці на співбесіді (Чого говорити не можна)
- ❌ «UnmodifiableList копіює елементи списку в новий масив» — груба помилка: це просто декоратор, що зберігає посилання.
- ❌ «UnmodifiableList потокобезпечний, і його можна сміливо віддавати в інші потоки» — ні, при зміні оригіналу паралельний ітератор впаде з CME.
- ❌ «UnmodifiableList захищає оригінальний список від додавання елементів» — ні, оригінальний список можна вільно модифікувати безпосередньо за оригінальним посиланням.