📦 Раздел 4 · Вопрос #7

Что такое Vector и чем он отличается от ArrayList

По своей внутренней структуре Vector аналогичен ArrayList (оба хранят элементы в Object[]), однако практически все публичные методы Vector помечены ключевым словом synchronized.


🟢 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 раза ($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 избыточно замедляет однопоточный код и создает жесткое состязание за монитор при многопоточном чтении. При этом Vector не защищает от гонок в составных операциях (check-then-act). В современном коде Vector не используется: для одного потока выбирают ArrayList, а для многопоточных задач — CopyOnWriteArrayList или конкурентные очереди».

Вопросы повышенной сложности с ответами

  1. Почему Vector не считается полноценно потокобезопасным классом, несмотря на то что все его методы синхронизированы? Ответ: Синхронизация методов Vector гарантирует потокобезопасность только отдельных изолированных вызовов (например, одного метода add или get). Однако в реальных приложениях операции почти всегда являются составными (Check-then-Act): например, проверка if (!vector.contains(e)) vector.add(e) или получение с удалением if (!vector.isEmpty()) vector.remove(0). Между проверкой и выполнением действия монитор освобождается, и другой поток может успеть модифицировать коллекцию, вызвав исключение или логическую ошибку. Для потокобезопасности составных операций программист всё равно вынужден синхронизировать весь блок кода внешним замком synchronized(vector).

  2. Как правильно организовать потокобезопасный обход элементов Vector? Ответ: Итератор vector.iterator() или цикл for-each бросают ConcurrentModificationException, если другой поток изменит Vector во время обхода. Чтобы безопасно обойти Vector в многопоточной среде, весь цикл итерации необходимо явно обернуть в синхронизированный блок по самому объекту вектора:
    synchronized (vector) {
        for (String item : vector) {
            process(item);
        }
    }
    
  3. Почему класс 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), вставляя и удаляя элементы в произвольных позициях, разрушая фундаментальный инвариант стека. Кроме того, наследование от Vector накладывает на все операции стека ненужные накладные расходы мониторов synchronized.

  4. В чём разница между 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 можно безопасно итерироваться в цикле foreach без внешнего synchronized. (Red Flag: непонимание механики Fail-Fast итераторов).
  • ❌ Использовать класс Stack для новых задач. (Вместо него всегда должен использоваться ArrayDeque).

Связанные темы