W3docs

Java Vector

La classe Vector synchronisée en Java, pourquoi elle est obsolète et dans quels rares cas l'utiliser encore.

Vector<E> est le List redimensionnable original basé sur un tableau — il a été livré avec Java 1.0, quatre ans avant l'existence du Collections Framework. Quand Java 1.2 a ajouté ArrayList, Vector a été soigneusement adapté pour implémenter List afin que le code existant utilisant Vector ne soit pas cassé. Trois décennies plus tard, il est toujours dans la bibliothèque standard, toujours fonctionnel, et toujours le mauvais choix pour presque tout nouveau code. Ce chapitre est volontairement court : vous devez savoir ce qu'est Vector pour le reconnaître dans l'ancien code, et non parce que vous écrirez du nouveau code qui l'utilise.

Ce qui est réellement différent d'ArrayList

Vector est un List soutenu par un tableau redimensionnable, tout comme ArrayList. Deux différences importent :

  • Chaque méthode publique est synchronized. Chaque add, get, set, remove, size, iterator — toutes acquièrent le moniteur du Vector à l'entrée. L'intention en 1995 était la sécurité des threads ; l'effet pratique est un verrouillage par méthode qui est grossier, lent, et rarement correct (plus de détails ci-dessous).
  • La politique de croissance est différente. Par défaut, quand le tableau de support se remplit, Vector le double. ArrayList croît d'environ 50 %. Le doublement gaspille plus de mémoire en moyenne ; une croissance de 50 % en gaspille moins. Aucun des deux ne compte en pratique à moins de gérer des millions de petites listes.

C'est tout. Chaque autre comportement observable est le même : accès aléatoire en O(1), insertion au début en O(n), itérateurs fail-fast, même interface générique.

Pourquoi "thread-safe" ne suffit pas

Le synchronized par méthode est exactement autant de synchronisation qu'un appel unique en a besoin, et exactement la mauvaise granularité pour tout le reste. Considérez le check-then-act :

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

Deux appels Vector sont chacun atomiques. La combinaison ne l'est pas. Entre la vérification contains et le add, un autre thread peut s'insérer avec un add concurrent. Le verrou que vous vouliez est celui qui couvre les deux appels, pas chaque appel individuellement. Pour l'obtenir, vous écrivez synchronized (v) { ... } autour du bloc entier — à ce stade, vous avez reproduit ce que Collections.synchronizedList(arrayList) fait déjà, mais sur une classe plus ancienne et maladroite.

Le même piège détruit l'itération :

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

Une mutation concurrente au milieu lance ConcurrentModificationException exactement comme pour ArrayList. Les mutateurs synchronisés n'aident pas ; l'itérateur ne tient pas le verrou entre les appels. Vous avez toujours besoin d'un synchronized (v) { ... } externe pour une itération sûre.

En résumé : la synchronisation par méthode n'apporte que très peu, et la contention de verrous qu'elle coûte est bien réelle. Les collections concurrentes à granularité fine de type ConcurrentHashMap (CopyOnWriteArrayList, ConcurrentLinkedDeque, etc.) sont ce que le code moderne utilise.

L'API propre à Vector que vous verrez

Un petit nombre de méthodes existent sur Vector et pas sur List. Ce sont des synonymes hérités, conservés pour la rétrocompatibilité :

Méthode VectorÉquivalent 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() (retourne l'ancienne Enumeration)
capacity()(aucun équivalent)
copyInto(Object[])toArray()

elements() est celle qui prend les gens par surprise — elle retourne Enumeration<E>, l'interface de traversée antérieure à Iterator. Si vous lisez du code qui appelle elements(), c'est un Vector (ou Hashtable) qui se révèle.

Quand Vector est acceptable dans du nouveau code

Honnêtement, très rarement. Deux cas se présentent :

  • Vous maintenez ou étendez du vieux code qui l'utilise déjà. Ne remaniez pas le code environnant juste pour échanger VectorArrayList — le gain ne vaut pas la différence. Le nouveau code dans le même module peut utiliser ArrayList.
  • Une API requiert spécifiquement Vector. Quelques classes Swing plus anciennes (DefaultTableModel de JTable, DefaultListModel de JList historiquement) prennent ou retournent des Vector. Utilisez ce que l'API demande à la frontière, puis convertissez si vous préférez travailler avec un List ailleurs.

Pour « j'ai besoin d'une liste thread-safe », les meilleurs choix sont :

  • Collections.synchronizedList(new ArrayList<>()) — même modèle de verrouillage par méthode, mais sur la classe moderne. Nécessite toujours un verrouillage externe pour les opérations composées et l'itération.
  • CopyOnWriteArrayList — lectures sans verrou, itération sûre sur un instantané. Excellent pour beaucoup-de-lecteurs-peu-d'écrivains (listes d'observateurs, écouteurs d'événements, caches de configuration quasi-immuables).

Pour « j'ai besoin des performances brutes d'une liste mono-thread », ArrayList. La surcharge synchronized sur Vector est faible mais non nulle, et il n'y a aucun avantage si aucun autre thread n'est impliqué.

Un exemple concret : ArrayList et Vector côte à côte

Le programme ci-dessous montre la correspondance d'API, les noms de méthodes hérités, et une petite démonstration que le synchronized par méthode n'est pas la même chose qu'une opération composée thread-safe. Lisez la note sur la synchronisation à la fin — c'est tout l'objet de ce chapitre.

java— editable, runs on the server

Ce que l'exécution montre :

  • ArrayList et Vector sont interchangeables à travers l'interface List — mêmes éléments, même égalité, même ordre d'itération.
  • Les méthodes propres à Vector (addElement, firstElement, elements, capacity) sont bien vivantes, ce qui explique pourquoi on les voit encore dans l'ancien code.
  • La démonstration de la condition de course est la raison même pour laquelle Vector est « hérité » : sa synchronisation est la mauvaise unité. Le nombre de 1 stockés est supérieur à un parce que la vérification et l'ajout ne sont pas atomiques ensemble.

La suite

L'autre survivant de l'ère 1.0 — construit sur Vector et héritant de tous ses défauts — est la classe Stack. C'est la deuxième étude de cas de « bonne idée, implémentation datée, remplacement moderne disponible ». Ce remplacement est Deque, que nous rencontrerons deux chapitres plus tard.

Pratique

Pratique
Un coéquipier écrit `if (!vector.contains(x)) vector.add(x);` pour ajouter `x` une seule fois depuis plusieurs threads, au motif que `Vector` est thread-safe. Qu'est-ce qui est réellement vrai ?
Un coéquipier écrit `if (!vector.contains(x)) vector.add(x);` pour ajouter `x` une seule fois depuis plusieurs threads, au motif que `Vector` est thread-safe. Qu'est-ce qui est réellement vrai ?
Was this page helpful?