Що таке Vector і чим він відрізняється від ArrayList
За своєю внутрішньою будовою Vector аналогічний до ArrayList (обидва зберігають елементи в масиві Object[]), проте практично всі публічні методи Vector позначені ключовим словом...
🟢 Junior Level
Vector — це застарілий клас динамічного масиву зі складу JDK 1.0, який у Java 2 (JDK 1.2) був адаптований для реалізації інтерфейсу List.
За своєю внутрішньою будовою Vector аналогічний до ArrayList (обидва зберігають елементи в масиві Object[]), проте практично всі публічні методи Vector позначені ключовим словом synchronized.
Головні відмінності між Vector та ArrayList
| Критерій | Vector |
ArrayList |
|---|---|---|
| Потокобезпечність | Синхронізований (методи містять synchronized) |
Не синхронізований |
| Продуктивність | Низька (накладні витрати на захоплення блокувань) | Висока (відсутність блокувань) |
| Коефіцієнт розширення | Подвоює розмір ($2.0\times$) за замовчуванням | Збільшує розмір в $1.5\times$ |
| Налаштування розширення | Можна задати точний крок capacityIncrement |
Фіксований крок $1.5\times$ |
| Застарілі інтерфейси | Підтримує Enumeration та Iterator |
Тільки Iterator та Spliterator |
| Версія появи | Java 1.0 (Legacy) | Java 1.2 (Java Collections Framework) |
// ❌ АНТИПАТЕРН: Використання Vector у новому коді
List<String> vector = new Vector<>();
// ✅ ПРАВИЛЬНО: ArrayList для однопотокового використання
List<String> list = new ArrayList<>();
// ✅ ПРАВИЛЬНО: Потокобезпечні сучасні альтернативи
List<String> cowList = new CopyOnWriteArrayList<>();
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
🟡 Middle Level
1. Чому синхронізація Vector — це ілюзія безпеки?
Наявність synchronized на кожному окремому методі захищає лише атомарність одиничної операції (наприклад, один виклик add або один get). Проте вона не захищає від стану гонитви (Race Condition) у складених операціях (Check-then-Act):
// ❌ НЕПОТОКОБЕЗПЕЧНИЙ КОД ІЗ VECTOR:
if (!vector.isEmpty()) {
// У цей момент інший потік встигає викликати vector.clear() або vector.remove(0)
String item = vector.remove(0); // Викине ArrayIndexOutOfBoundsException!
}
// ❌ Те саме під час перевірки на дублікати:
if (!vector.contains(element)) {
vector.add(element); // Два потоки можуть одночасно додати дублікат!
}
Щоб зробити складену операцію дійсно потокобезпечною, розробник все одно зобов’язаний використовувати зовнішню синхронізацію:
synchronized (vector) {
if (!vector.isEmpty()) {
String item = vector.remove(0);
}
}
Але якщо для безпеки все одно необхідний зовнішній блок synchronized, то внутрішні блокування кожного методу Vector перетворюються на абсолютно марне гальмо, яке сповільнює виконання в рази.
2. Ітерація по Vector: ризик ConcurrentModificationException
Багато хто помилково вважає, що елементи Vector можна безпечно обходити з різних потоків:
// ❌ ВИКЛИЧЕ ConcurrentModificationException В ІНШОМУ ПОТОЦІ!
for (String s : vector) { // Викликає vector.iterator()
System.out.println(s);
}
Метод iterator() у Vector повертає стандартний Fail-Fast ітератор, який перевіряє лічильник модифікацій modCount. Якщо під час ітерації інший потік викличе vector.add(), ітератор негайно викине ConcurrentModificationException. Будь-яку ітерацію по Vector у багатопотоковому середовищі обов’язково необхідно огортати в зовнішній блок synchronized(vector)!
3. Різниця в алгоритмі розширення (Resizing)
// Усередині Vector:
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
- Якщо в конструкторі
new Vector<>(initialCapacity, capacityIncrement)не вказано параметр приросту, місткість подвоюється ($2.0\times$). Це призводить до швидшого зростання невикористаної пам’яті в купі порівняно з $1.5\times$ вArrayList. - В
ArrayListможливості задати фіксований крок розширення немає — він завжди множить розмір на $\approx 1.5$.
🔴 Senior Level
1. Проблема суперництва за блокування (Lock Contention)
У Vector всі методи синхронізуються на одному спільному моніторі екземпляра (this):
- Навіть якщо 50 потоків виконують виключно операції читання
vector.get(i), вони не здатні читати дані паралельно! - Потоки шикуються в чергу до єдиного монітора, викликаючи колосальний простій процесора (Lock Contention) та зводячи паралелізм нанівець.
Для багатопотокових сценаріїв у сучасній Java застосовують:
CopyOnWriteArrayList: операції читання виконуються взагалі без блокувань (Lock-Free), а запис копіює підкладний масив. Ідеально підходить за співвідношення читань до записів 99 до 1.- Блокувальні черги (
LinkedBlockingQueue,ArrayBlockingQueue): використовують роздільні блокування на читання і запис (takeLockтаputLock) і умовні змінні (Condition).
2. Порушення принципу підстановки Лісков (LSP) у java.util.Stack
Клас java.util.Stack є класичним прикладом архітектурної помилки проєктування стандартної бібліотеки Java: його було оголошено як public class Stack<E> extends Vector<E>.
Stack<String> stack = new Stack<>();
stack.push("Нижній");
stack.push("Верхній");
// ❌ Грубе порушення контракту стека (LIFO):
stack.add(1, "Вклинений елемент"); // Метод Vector порушує інваріант LIFO!
stack.remove(0); // Видалення із самого дна стека!
- Згідно з принципом Лісков (LSP), підклас зобов’язаний коректно замінювати базовий тип. Проте стек не є окремим випадком динамічного масиву: він повинен забороняти довільний доступ за індексом.
- Рішення: замість
Stackзавжди слід використовувати інтерфейсDequeта реалізаціюArrayDeque(Deque<String> stack = new ArrayDeque<>()).
3. Enumeration проти Iterator
Vector зберігає історичний метод elements(), який повертає інтерфейс java.util.Enumeration<E>:
Enumeration— інтерфейс JDK 1.0 із методамиhasMoreElements()таnextElement();- НЕ є Fail-Fast: він не відстежує лічильник
modCountі під час паралельної модифікації колекції може повернути некоректні дані або взагалі зациклитися; - Не містить методу безпечного видалення
remove().
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь для співбесіди
«
Vector— це застарілий потокобезпечний аналогArrayListіз JDK 1.0, у якому кожен публічний метод синхронізований за монітором екземпляра (synchronized). За замовчуваннямVectorподвоює місткість ($2.0\times$), тоді якArrayListзбільшує її в $1.5$ раза. Синхронізація методівVectorнадлишково уповільнює однопотоковий код і створює жорстке суперництво за блокування (Lock Contention) під час багатопотокового читання. До того жVectorне захищає від гонитв у складених операціях (check-then-act). У сучасному кодіVectorне використовується: для одного потоку обираютьArrayList, а для багатопотокових завдань —CopyOnWriteArrayListабо конкурентні черги».
Питання підвищеної складності з відповідями
-
Чому
Vectorне вважається повноцінно потокобезпечним класом, попри те що всі його методи синхронізовані? Відповідь: Синхронізація методівVectorгарантує потокобезпечність лише окремих ізольованих викликів (наприклад, одного методуaddабоget). Проте в реальних застосунках операції майже завжди є складеними (Check-then-Act): наприклад, перевіркаif (!vector.contains(e)) vector.add(e)або вилученняif (!vector.isEmpty()) vector.remove(0). Між перевіркою та виконанням дії монітор вивільняється, й інший потік може встигнути змінити стан колекції, викликавши виняток або логічну помилку. Для безпеки складених операцій розробник все одно змушений синхронізувати весь блок коду зовнішнім блокуваннямsynchronized(vector). - Як правильно організувати потокобезпечний обхід елементів
Vector? Відповідь: Ітераторvector.iterator()або циклfor-eachвикидаютьConcurrentModificationException, якщо інший потік змінитьVectorпід час обходу. Щоб безпечно обійтиVectorу багатопотоковому середовищі, весь цикл ітерації необхідно явно огорнути в синхронізований блок за самим об’єктом вектора:synchronized (vector) { for (String item : vector) { process(item); } } -
Чому клас
java.util.Stackвважається невдалим архітектурним рішенням у Java? Відповідь:Stackнапряму успадковуєVector(class Stack<E> extends Vector<E>), що порушує принцип підстановки Барбари Лісков (LSP) та принцип єдиної відповідальності. Стек повинен підтримувати лише операції з вершиною (LIFO:push,pop,peek). Проте через успадкування відVectorклієнт стека може викликатиstack.add(index, e)абоstack.remove(0), довільно порушуючи фундаментальний інваріант LIFO. Крім того, спадкування відVectorнакладає на всі виклики стека непотрібні накладні витрати моніторівsynchronized. - У чому різниця між
EnumerationізVectorта стандартнимIterator? Відповідь:Enumeration— застарілий інтерфейс із JDK 1.0 (hasMoreElements(),nextElement()). Він не підтримує видалення елементів під час обходу (немає методуremove()) і, найголовніше, він не є Fail-Fast — він не перевіряє лічильник модифікаційmodCount, тому при модифікації колекції іншим потоком може зациклитися або повернути спотворені дані. СучаснийIteratorє fail-fast і негайно викидаєConcurrentModificationException.
Типові помилки та Red Flags
- ❌ Стверджувати, що
Vectorслід використовувати завжди, коли є кілька потоків. (Red Flag: незнанняjava.util.concurrentта проблем блокування моніторів). - ❌ Думати, що елементи
Vectorможна безпечно обходити у циклі for-each без зовнішньогоsynchronized. (Red Flag: нерозуміння механіки Fail-Fast ітераторів). - ❌ Використовувати клас
Stackдля нових завдань. (Замість нього завжди слід застосовуватиArrayDeque).