Як працює Collections.unmodifiableList() всередині
Collections.unmodifiableList() повертає обгортку (wrapper) поверх вихідного списку, яка забороняє всі модифікуючі операції.
🟢 Junior Level
Collections.unmodifiableList() повертає обгортку (wrapper) поверх вихідного списку, яка забороняє всі модифікуючі операції.
List<String> mutable = new ArrayList<>();
mutable.add("A");
mutable.add("B");
List<String> unmodifiable = Collections.unmodifiableList(mutable);
unmodifiable.add("C"); // ❌ UnsupportedOperationException!
unmodifiable.get(0); // ✅ OK — читання працює
Ключовий момент: unmodifiableList — це не копія, а обгортка. Якщо змінити оригінальний список, зміни будуть видні через обгортку:
mutable.add("C");
System.out.println(unmodifiable); // [A, B, C] — зміну видно!
🟡 Middle Level
Внутрішня реалізація
Collections.unmodifiableList() повертає об’єкт внутрішнього класу Collections.UnmodifiableList:
public static <T> List<T> unmodifiableList(List<? extends T> list) {
return (list instanceof RandomAccess)
? new UnmodifiableRandomAccessList<>(list)
: new UnmodifiableList<>(list);
}
Два варіанти обгортки:
UnmodifiableRandomAccessList— якщо список підтримує довільний доступ (ArrayList)UnmodifiableList— якщо ні (LinkedList)
Це оптимізація продуктивності: RandomAccess-списки використовують індексний доступ O(1), а не ітератор.
Структура UnmodifiableList
static class UnmodifiableList<E> extends UnmodifiableCollection<E>
implements List<E> {
private final List<? extends E> list;
UnmodifiableList(List<? extends E> list) {
super(list);
this.list = list; // посилання на оригінал, не копія!
}
// Усі модифікуючі методи кидають UnsupportedOperationException
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 E get(int index) {
return list.get(index); // прямий виклик до оригіналу
}
public int size() {
return list.size();
}
}
UnmodifiableList vs List.copyOf (Java 10+)
| Характеристика | unmodifiableList() |
List.copyOf() |
|---|---|---|
| Копіювання | Ні (обгортка) | Так (defensive copy) |
| Зміна оригіналу | Видно через обгортку | Не впливає |
| Null елементи | Дозволені | Заборонені |
| Тип повернення | UnmodifiableList |
ImmutableCollections.ListN |
| Версія Java | З Java 1.2 | З Java 10 |
RandomAccess — маркерний інтерфейс, що означає O(1) доступ за індексом. Defensive copy — копіювання даних для захисту від зовнішніх змін.
List<String> original = new ArrayList<>(List.of("A", "B"));
// unmodifiableList — обгортка
List<String> wrapper = Collections.unmodifiableList(original);
original.add("C");
System.out.println(wrapper); // [A, B, C] — зміну видно!
// List.copyOf — копія
List<String> copy = List.copyOf(original);
original.add("D");
System.out.println(copy); // [A, B, C] — зміну НЕ видно
ListIterator обгортка
ListIterator<String> it = unmodifiable.listIterator();
it.next(); // OK
it.set("X"); // ❌ UnsupportedOperationException
it.add("Y"); // ❌ UnsupportedOperationException
it.remove(); // ❌ UnsupportedOperationException
Повертається UnmodifiableListIterator, який також блокує всі модифікації.
Продуктивність
| Операція | ArrayList напряму |
unmodifiableList |
|---|---|---|
get(i) |
O(1) | O(1) (один виклик делегування) |
size() |
O(1) | O(1) (делегування) |
contains() |
O(n) | O(n) (делегування) |
iterator() |
O(1) | O(1) (обгортка ітератора) |
Overhead мінімальний: один рівень делегування (~1-2 ns на виклик).
🔴 Senior Level
Глибокий аналіз вихідного коду JDK
// Collections.java (OpenJDK)
static class UnmodifiableCollection<E> implements Collection<E>, Serializable {
final Collection<? extends E> c;
UnmodifiableCollection(Collection<? extends E> c) {
if (c == null) throw new NullPointerException();
this.c = c;
}
// Усі mutating-методи кидають виняток:
public boolean add(E e) { throw new UnsupportedOperationException(); }
public boolean remove(Object o) { throw new UnsupportedOperationException(); }
public void clear() { throw new UnsupportedOperationException(); }
// read-методи делегують:
public int size() { return c.size(); }
public boolean isEmpty() { return c.isEmpty(); }
public boolean contains(Object o) { return c.contains(o); }
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(); }
};
}
}
UnmodifiableRandomAccessList оптимізація
static class UnmodifiableRandomAccessList<E>
extends UnmodifiableList<E>
implements RandomAccess {
UnmodifiableRandomAccessList(List<? extends E> list) {
super(list);
}
// Перевизначає subList — теж повертає unmodifiable
public List<E> subList(int from, int to) {
return new UnmodifiableRandomAccessList<>(
list.subList(from, to)
);
}
}
Ця оптимізація важлива: UnmodifiableList (не-RandomAccess) конвертує індексні операції в ітераторні, що для ArrayList було б O(n) замість O(1).
Через рефлексію можна змінити!
List<String> list = new ArrayList<>(List.of("A", "B"));
List<String> unmod = Collections.unmodifiableList(list);
// Через рефлексію — доступ до поля `list` в UnmodifiableList
Field field = unmod.getClass().getDeclaredField("list");
field.setAccessible(true);
List<String> underlying = (List<String>) field.get(unmod);
underlying.add("C"); // Зміна обходить захист!
System.out.println(unmod); // [A, B, C]
Це не баг, а особливість wrapper-підходу. List.copyOf() (Java 10+) вирішує цю проблему, створюючи справжню незмінювану колекцію.
Якщо ваш проект на Java 8 — List.copyOf() недоступний. Використовуйте Collections.unmodifiableList(new ArrayList<>(original)) як defensive copy.
Серіалізація
UnmodifiableList реалізує Serializable. При десеріалізації:
// Відновлюється обгортка з посиланням на десеріалізований список
// Якщо оригінал змінився після серіалізації обгортки — це НЕ вплине
Коли використовувати
| Сценарій | Рекомендація |
|---|---|
| API для читання, оригінал контролюється вами | unmodifiableList() |
| Повна іммутабельність, Java 10+ | List.copyOf() |
| Thread-safe незмінюваний список | List.copyOf() або unmodifiableList() + синхронізація оригіналу |
| Глибока іммутабельність (вкладені об’єкти) | Жоден із цих методів — використовуйте immutable DTO |
Best Practices
// ✅ Повернення unmodifiable з публічного API
public List<String> getNames() {
return Collections.unmodifiableList(names);
}
// ✅ Java 10+ — List.copyOf для повної іммутабельності
public List<String> getNames() {
return List.copyOf(names);
}
// ❌ Не використовуйте unmodifiableList для захисту від конкурентних модифікацій
// Для цього — CopyOnWriteArrayList або Collections.synchronizedList()
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
unmodifiableList()повертаєUnmodifiableRandomAccessListабоUnmodifiableListзалежно відRandomAccess- Усі модифікуючі методи кидають
UnsupportedOperationException - Читаючі методи делегують оригіналу напряму — overhead 1-2 ns
subList()теж повертає unmodifiable обгортку- Через рефлексію можна дістатися до оригіналу і обійти захист
List.copyOf()(Java 10+) — справжня іммутабельність, defensive copy, без зв’язку з оригіналом- Iterator/ListIterator також обгорнуті в unmodifiable-версії (remove/set/add заборонені)
Часті уточнюючі запитання:
- Чому два класи — UnmodifiableList і UnmodifiableRandomAccessList? — Оптимізація: RandomAccess-списки отримують індексний доступ O(1), решта — через ітератор.
- Чи можна обійти захист unmodifiableList? — Так, через рефлексію (
getDeclaredField("list")).List.copyOf()вирішує цю проблему. - Який overhead при виклику методів обгортки? — Один рівень делегування, ~1-2 ns — практично нульовий.
- Що поверне
subList()unmodifiable-списку? — Ще одну unmodifiable-обгортку на подсписок.
Червоні прапорці (НЕ говорити):
- «unmodifiableList створює копію колекції» — ні, це wrapper-патерн, зберігає посилання на оригінал
- «Захист від змін гарантований на 100%» — рефлексія дозволяє обійти
- «unmodifiableList thread-safe» — ні, синхронізацію потрібно забезпечувати окремо
- «Iterator від unmodifiableList дозволяє видаляти елементи» — ні,
UnmodifiableListIteratorтеж блокує модифікації
Пов’язані теми:
- [[23. Що таке Collections.unmodifiableList()]]
- [[25. В чому різниця між Iterator та ListIterator]]
- [[22. Як отримати synchronized колекцію]]