W3docs

Java Vector

La classe Vector sincronizzata in Java, perché è legacy e quando (raramente) usarla ancora.

Vector<E> è il List a array ridimensionabile originale — è arrivato con Java 1.0, quattro anni prima che esistesse il Collections Framework. Quando Java 1.2 aggiunse ArrayList, fu accuratamente adattato per implementare List in modo che il codice Vector esistente non si rompesse. Tre decenni dopo è ancora nella libreria standard, ancora funzionante, e ancora la scelta sbagliata per quasi ogni nuovo pezzo di codice. Questo capitolo è volutamente breve: devi sapere cos'è Vector per riconoscerlo nel codice vecchio, non perché scriverai nuovo codice che lo usa.

Cosa è effettivamente diverso da ArrayList

Vector è un List supportato da un array ridimensionabile, proprio come ArrayList. Due differenze contano:

  • Ogni metodo pubblico è synchronized. Ogni add, get, set, remove, size, iterator — acquisiscono tutti il monitor del Vector all'ingresso. L'intenzione nel 1995 era la thread safety; l'effetto pratico è un locking per-metodo grezzo, lento e raramente corretto (vedi sotto).
  • La politica di crescita è diversa. Per impostazione predefinita, quando l'array sottostante si riempie, Vector lo raddoppia. ArrayList cresce di circa il 50%. Il raddoppio spreca più memoria in media; la crescita del 50% ne spreca meno. In pratica nessuna delle due è importante a meno che tu non gestisca milioni di liste piccole.

Questo è tutto. Ogni altro comportamento osservabile è lo stesso: accesso casuale O(1), inserimento in testa O(n), iteratori fail-fast, stessa interfaccia generica.

Perché "thread-safe" non è sufficiente

Il synchronized per-metodo è esattamente la quantità di sincronizzazione di cui ha bisogno una singola chiamata, ed esattamente la granularità sbagliata per tutto il resto. Considera check-then-act:

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

Due chiamate a Vector sono ciascuna atomica. La combinazione non lo è. Tra il controllo contains e la add, un altro thread può intromettersi con una add concorrente. Il lock che volevi è quello che copre entrambe le chiamate, non ciascuna singolarmente. Per ottenerlo scrivi synchronized (v) { ... } attorno all'intero blocco — a quel punto hai replicato ciò che Collections.synchronizedList(arrayList) fa già, solo su una classe più vecchia e scomoda.

La stessa trappola colpisce l'iterazione:

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

Una mutazione concorrente nel mezzo lancia ConcurrentModificationException esattamente come fa con ArrayList. I mutatori sincronizzati non aiutano; l'iteratore non mantiene il lock tra le chiamate. Hai ancora bisogno di un synchronized (v) { ... } esterno per un'iterazione sicura.

In breve: la sincronizzazione per-metodo ottiene molto poco, e la contesa di lock che costa è reale. Le collezioni concorrenti a grana fine in stile ConcurrentHashMap (CopyOnWriteArrayList, ConcurrentLinkedDeque, ecc.) sono quelle a cui ricorre il codice moderno.

L'API esclusiva di Vector che vedrai

Un manciata di metodi esistono su Vector e non su List. Sono sinonimi legacy, mantenuti per compatibilità con le versioni precedenti:

Metodo VectorEquivalente 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() (restituisce il più vecchio Enumeration)
capacity()(nessun equivalente)
copyInto(Object[])toArray()

elements() è quello che coglie le persone di sorpresa — restituisce Enumeration<E>, l'interfaccia di attraversamento precedente a Iterator. Se stai leggendo codice che chiama elements(), quello è un Vector (o Hashtable).

Quando Vector è accettabile nel nuovo codice

Onestamente, molto raramente. Due casi che si presentano:

  • Stai mantenendo o estendendo codice vecchio che già lo usa. Non rimescolare il codice circostante solo per scambiare VectorArrayList — il guadagno non vale la diff. Il nuovo codice nello stesso modulo può usare ArrayList.
  • Un'API richiede Vector specificamente. Alcune classi Swing più vecchie (DefaultTableModel di JTable, DefaultListModel di JList storicamente) prendono o restituiscono Vector. Usa ciò che l'API richiede al confine, poi converti se preferisci lavorare con un List altrove.

Per "ho bisogno di una lista thread-safe," le scelte migliori sono:

  • Collections.synchronizedList(new ArrayList<>()) — stesso modello di locking per-metodo, ma sulla classe moderna. Richiede ancora locking esterno per operazioni composte e iterazione.
  • CopyOnWriteArrayList — letture lock-free, iterazione sicura su uno snapshot. Eccellente per molti-lettori-pochi-scrittori (liste di observer, listener di eventi, cache di configurazione quasi-immutabili).

Per "ho bisogno di prestazioni raw di lista single-thread," ArrayList. L'overhead di synchronized su Vector è piccolo ma non zero, e non c'è nessun vantaggio se nessun altro thread è coinvolto.

Un esempio pratico: ArrayList e Vector affiancati

Il programma seguente mostra il mirror dell'API, i nomi dei metodi legacy, e una piccola dimostrazione che synchronized per-metodo non è la stessa cosa di un'operazione composta thread-safe. Leggi la nota sulla sincronizzazione alla fine — è il punto centrale del capitolo.

java— editable, runs on the server

Cosa mostra l'esecuzione:

  • ArrayList e Vector sono intercambiabili attraverso l'interfaccia List — stessi elementi, stessa uguaglianza, stesso ordine di iterazione.
  • I metodi esclusivi di Vector (addElement, firstElement, elements, capacity) sono vivi e funzionanti, motivo per cui li vedi ancora nel codice vecchio.
  • La dimostrazione della race condition è il motivo principale per cui Vector è "legacy": la sua sincronizzazione è nell'unità sbagliata. Il numero di 1 memorizzati è maggiore di uno perché il controllo e l'aggiunta non sono atomici insieme.

Cosa viene dopo

L'altro sopravvissuto dell'era 1.0 — costruito sopra Vector ed ereditando tutti i suoi difetti — è la classe Stack. È il secondo caso di studio in "idea utile, implementazione datata, sostituto moderno disponibile." Quel sostituto è Deque, che incontreremo due capitoli dopo.

Pratica

Pratica
Un collega scrive `if (!vector.contains(x)) vector.add(x);` per aggiungere `x` solo una volta da più thread, sostenendo che `Vector` è thread-safe. Cosa è effettivamente vero?
Un collega scrive `if (!vector.contains(x)) vector.add(x);` per aggiungere `x` solo una volta da più thread, sostenendo che `Vector` è thread-safe. Cosa è effettivamente vero?
Was this page helpful?