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. Не переписывайте окружающий код только ради заменыVector→ArrayList— выгода не стоит изменений. Новый код в том же модуле может использоватьArrayList. - API требует именно
Vector. Несколько старых Swing-классов (DefaultTableModelдляJTable,DefaultListModelдляJListисторически) принимают или возвращаютVector. Используйте то, что требует API на границе, а потом конвертируйте, если предпочитаете работать сListв остальном коде.
Для "мне нужен потокобезопасный список" лучшие варианты:
Collections.synchronizedList(new ArrayList<>())— та же модель блокировки на уровне метода, но на современном классе. Всё равно требует внешней блокировки для составных операций и итерации.CopyOnWriteArrayList— чтение без блокировки, безопасная итерация по снимку. Отлично подходит для сценариев много-читателей-мало-писателей (списки наблюдателей, обработчики событий, почти неизменяемые кэши конфигурации).
Для "мне нужна максимальная производительность однопоточного списка" — ArrayList. Накладные расходы от synchronized в Vector невелики, но ненулевые, и они не дают никакого преимущества, если другие потоки не участвуют.
Практический пример: ArrayList и Vector рядом
Программа ниже демонстрирует зеркальность API, устаревшие имена методов и небольшой пример того, что синхронизация на уровне метода — это не то же самое, что потокобезопасная составная операция. Обратите внимание на замечание о синхронизации в конце — в этом и есть весь смысл главы.
Что показывает запуск:
ArrayListиVectorвзаимозаменяемы через интерфейсList— те же элементы, то же равенство, тот же порядок итерации.- Методы, специфичные для
Vector(addElement,firstElement,elements,capacity), живы и работают — вот почему вы до сих пор видите их в старом коде. - Демонстрация гонки — вот главная причина, по которой
Vectorсчитается "устаревшим": его синхронизация имеет неверный масштаб. Количество сохранённых1больше одного, потому что проверка и добавление вместе не являются атомарными.
Что дальше
Другой выживший из эпохи Java 1.0 — построенный поверх Vector и унаследовавший все его недостатки — это класс Stack. Это второй пример "полезная идея, устаревшая реализация, современная замена доступна". Этой заменой является Deque, с которым мы познакомимся двумя главами позже.