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. Ogniadd,get,set,remove,size,iterator— acquisiscono tutti il monitor delVectorall'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,
Vectorlo raddoppia.ArrayListcresce 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 togetherDue 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 togetherUna 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 Vector | Equivalente 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
Vector→ArrayList— il guadagno non vale la diff. Il nuovo codice nello stesso modulo può usareArrayList. - Un'API richiede
Vectorspecificamente. Alcune classi Swing più vecchie (DefaultTableModeldiJTable,DefaultListModeldiJListstoricamente) prendono o restituisconoVector. Usa ciò che l'API richiede al confine, poi converti se preferisci lavorare con unListaltrove.
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.
Cosa mostra l'esecuzione:
ArrayListeVectorsono intercambiabili attraverso l'interfacciaList— 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 di1memorizzati è 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.