W3docs

Java Vector

Синхронизированный класс Vector в Java: почему он устарел и когда его ещё можно использовать.

Vector<E> — это оригинальный List на основе изменяемого массива: он появился в Java 1.0, за четыре года до появления Collections Framework. Когда в Java 1.2 добавили ArrayList, Vector был аккуратно адаптирован для реализации интерфейса List, чтобы существующий код на Vector не сломался. Спустя три десятилетия он по-прежнему находится в стандартной библиотеке, по-прежнему работает и по-прежнему является неверным выбором почти для любого нового кода. Эта глава намеренно короткая: вам нужно знать, что такое Vector, чтобы узнавать его в старом коде, а не потому что вы будете писать новый код с его использованием.

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

Vector — это List на основе изменяемого массива, точно как ArrayList. Есть два важных отличия:

  • Каждый публичный метод является synchronized. Каждый add, get, set, remove, size, iterator — все они захватывают монитор Vector при входе. Намерение в 1995 году состояло в потокобезопасности; практический эффект — грубая блокировка на уровне метода, медленная и редко корректная (подробнее ниже).
  • Политика роста отличается. По умолчанию, когда внутренний массив заполняется, Vector удваивает его размер. ArrayList увеличивается примерно на 50%. Удвоение в среднем тратит больше памяти; рост на 50% — меньше. На практике это не важно, если вы не управляете миллионами небольших списков.

Вот и всё. Всё остальное поведение одинаково: случайный доступ за O(1), вставка в начало за O(n), итераторы с быстрым сбоем, одинаковый обобщённый интерфейс.

Почему "потокобезопасный" — это недостаточно

Синхронизация на уровне метода — это ровно столько синхронизации, сколько нужно для одного вызова, и ровно неверная гранулярность для всего остального. Рассмотрим паттерн "проверить-затем-действовать":

Vector<String> v = ...;
if (!v.contains("hello")) {     // synchronised → atomic
  v.add("hello");                // synchronised → atomic
}                                 // BUT: NOT atomic together

Каждый из двух вызовов Vector является атомарным. Их комбинация — нет. Между проверкой contains и вызовом add другой поток может незаметно выполнить конкурирующий add. Нужная вам блокировка — та, которая охватывает оба вызова, а не каждый по отдельности. Чтобы её получить, нужно написать synchronized (v) { ... } вокруг всего блока — в этот момент вы воспроизводите то, что Collections.synchronizedList(arrayList) уже делает, только на более неудобном старом классе.

Та же ловушка губительна для итерации:

for (String s : v) { ... }   // many internal hasNext/next calls, none locked together

Параллельная мутация в середине бросает ConcurrentModificationException точно так же, как и для ArrayList. Синхронизированные мутаторы не помогают; итератор не удерживает блокировку между вызовами. Для безопасной итерации всё равно нужен внешний synchronized (v) { ... }.

Короче говоря: синхронизация на уровне метода даёт очень мало, а накладные расходы от конкуренции за блокировку реальны. Именно для этого в современном коде используются мелкозернистые конкурентные коллекции в стиле ConcurrentHashMap (CopyOnWriteArrayList, ConcurrentLinkedDeque и т.д.).

Методы API, специфичные для Vector

Несколько методов существуют в Vector, но не в List. Это устаревшие синонимы, оставленные для обратной совместимости:

Метод VectorЭквивалент в List
addElement(E)add(E)
insertElementAt(E, int)add(int, E)
removeElement(Object)remove(Object)
removeElementAt(int)remove(int)
elementAt(int)get(int)
setElementAt(E, int)set(int, E)
firstElement() / lastElement()get(0) / get(size()-1)
elements()iterator() (возвращает устаревший Enumeration)
capacity()(нет эквивалента)
copyInto(Object[])toArray()

elements() — тот метод, который чаще всего сбивает с толку: он возвращает Enumeration<E>, интерфейс обхода, предшествующий Iterator. Если вы видите в коде вызов elements(), это признак Vector (или Hashtable).

Когда Vector допустим в новом коде

Честно говоря, очень редко. Есть два случая:

  • Вы поддерживаете или расширяете старый код, который уже использует Vector. Не переписывайте окружающий код только ради замены VectorArrayList — выгода не стоит изменений. Новый код в том же модуле может использовать ArrayList.
  • API требует именно Vector. Несколько старых Swing-классов (DefaultTableModel для JTable, DefaultListModel для JList исторически) принимают или возвращают Vector. Используйте то, что требует API на границе, а потом конвертируйте, если предпочитаете работать с List в остальном коде.

Для "мне нужен потокобезопасный список" лучшие варианты:

  • Collections.synchronizedList(new ArrayList<>()) — та же модель блокировки на уровне метода, но на современном классе. Всё равно требует внешней блокировки для составных операций и итерации.
  • CopyOnWriteArrayList — чтение без блокировки, безопасная итерация по снимку. Отлично подходит для сценариев много-читателей-мало-писателей (списки наблюдателей, обработчики событий, почти неизменяемые кэши конфигурации).

Для "мне нужна максимальная производительность однопоточного списка" — ArrayList. Накладные расходы от synchronized в Vector невелики, но ненулевые, и они не дают никакого преимущества, если другие потоки не участвуют.

Практический пример: ArrayList и Vector рядом

Программа ниже демонстрирует зеркальность API, устаревшие имена методов и небольшой пример того, что синхронизация на уровне метода — это не то же самое, что потокобезопасная составная операция. Обратите внимание на замечание о синхронизации в конце — в этом и есть весь смысл главы.

java— editable, runs on the server

Что показывает запуск:

  • ArrayList и Vector взаимозаменяемы через интерфейс List — те же элементы, то же равенство, тот же порядок итерации.
  • Методы, специфичные для Vector (addElement, firstElement, elements, capacity), живы и работают — вот почему вы до сих пор видите их в старом коде.
  • Демонстрация гонки — вот главная причина, по которой Vector считается "устаревшим": его синхронизация имеет неверный масштаб. Количество сохранённых 1 больше одного, потому что проверка и добавление вместе не являются атомарными.

Что дальше

Другой выживший из эпохи Java 1.0 — построенный поверх Vector и унаследовавший все его недостатки — это класс Stack. Это второй пример "полезная идея, устаревшая реализация, современная замена доступна". Этой заменой является Deque, с которым мы познакомимся двумя главами позже.

Практика

Практика
Коллега пишет `if (!vector.contains(x)) vector.add(x);`, чтобы безопасно добавить `x` только один раз из нескольких потоков, ссылаясь на то, что `Vector` является потокобезопасным. Что на самом деле верно?
Коллега пишет `if (!vector.contains(x)) vector.add(x);`, чтобы безопасно добавить `x` только один раз из нескольких потоков, ссылаясь на то, что `Vector` является потокобезопасным. Что на самом деле верно?
Was this page helpful?