Java ArrayDeque
Utilisez ArrayDeque pour des files et piles à tableau circulaire redimensionnable et performantes en Java.
ArrayDeque<E> est un tableau circulaire qui s'agrandit à la demande. Il implémente à la fois Queue<E> et Deque<E>, ce qui lui permet de jouer le rôle de file FIFO, de pile LIFO ou de file à double extrémité sans modifier le code. La Javadoc de Stack recommande Deque plutôt que la classe héritée ; la Javadoc de Deque ajoute la recommandation pratique qu'ArrayDeque devrait être votre implémentation par défaut. Il est presque toujours plus rapide que LinkedList, plus économe en mémoire, et c'est la référence de la bibliothèque standard dès lors qu'on n'a plus besoin de l'interface List.
Pourquoi un tableau circulaire
Une « file construite à partir d'un tableau » classique rencontre un problème : après quelques cycles d'ajout en queue et de suppression en tête, le tableau se remplit du côté de la queue tandis que des emplacements vides s'accumulent du côté de la tête. Une solution naïve consisterait à décaler tout vers la gauche à chaque retrait — O(n) par suppression, ce qui ruine les performances.
L'astuce du tableau circulaire résout ce problème. La classe conserve deux indices, head et tail, et les laisse se reboucler :
slots: [ . . . D E F G H A B C ]
^head ^... wraps to slot 0 ... ^tailaddFirst décrémente head, addLast incrémente tail, tous deux modulo la longueur du tableau. removeFirst et removeLast font l'inverse. Chaque opération est O(1) ; le seul coût est de doubler le tableau de support quand head entrerait en collision avec tail, ce qui est amorti sur l'ensemble des opérations.
Résultat : pas d'allocation par élément de nœud, pas de chaînage de pointeurs, accès favorable au cache. C'est la raison d'ingénierie pour laquelle ArrayDeque est généralement le choix le plus rapide pour les charges de travail de file et de pile.
Rappel sur l'interface Deque
Un Deque possède deux extrémités. Chaque opération existe en trois variantes :
| Opération | Première (tête) — exception | Première (tête) — valeur spéciale | Dernière (queue) — exception | Dernière (queue) — valeur spéciale |
|---|---|---|---|---|
| Insertion | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
| Suppression | removeFirst() | pollFirst() | removeLast() | pollLast() |
| Examen | getFirst() | peekFirst() | getLast() | peekLast() |
De plus, les synonymes de Queue et de pile se mappent sur l'extrémité tête :
| Méthode Queue | Équivalent Deque |
|---|---|
add(e) / offer(e) | addLast(e) / offerLast(e) |
remove() / poll() | removeFirst() / pollFirst() |
element() / peek() | getFirst() / peekFirst() |
| Méthode Stack | Équivalent Deque |
|---|---|
push(e) | addFirst(e) |
pop() | removeFirst() |
peek() | peekFirst() |
Ainsi, Deque est une interface unique qui expose à la fois une file et une pile, selon le nom sous lequel vous appelez l'extrémité tête. Nous avons couvert ces correspondances dans les chapitres Queue et Stack ; Deque est l'endroit où ils se rejoignent.
Ce qu'ArrayDeque ajoute en plus
En pratique, très peu de choses au niveau de l'API — c'est justement l'objectif. La classe expose honnêtement le contrat Deque : les deux extrémités, les trois styles d'erreur par opération, ainsi que clear(), iterator(), descendingIterator(), contains, size, isEmpty. L'itération avec l'itérateur standard se fait de la tête vers la queue ; descendingIterator est le moyen économique de parcourir de la queue vers la tête sans inverser.
Les constructeurs reflètent ceux d'ArrayList :
Deque<String> a = new ArrayDeque<>(); // initial capacity 16
Deque<String> b = new ArrayDeque<>(1_000); // pre-sized
Deque<String> c = new ArrayDeque<>(other); // from any Collection (head = first iter element)Pré-dimensionnez lorsque vous connaissez la charge de travail. La valeur par défaut de 16 est arrondie à la puissance de deux supérieure, et la croissance double le tableau — ainsi, un million d'appels à offerLast à partir d'un deque de taille par défaut déclenche environ 16 opérations d'agrandissement et de copie.
Les règles : pas de null, non thread-safe, fail-fast
Trois règles à intérioriser :
ArrayDequen'autorise pas les élémentsnull. Chaque insertion lèveNullPointerException. C'est ainsi quepollFirst()retournantnullpour une file vide reste sans ambiguïté.- Non thread-safe. Deux threads modifiant le même
ArrayDequele corromperont. Pour un usage concurrent, consultezConcurrentLinkedDeque(sans verrou, non borné) ouLinkedBlockingDeque(borné, bloquant). - Itération fail-fast. Modifier le deque en dehors de l'itérateur pendant l'itération lève
ConcurrentModificationException. UtilisezIterator.remove()ouremoveIfpour une mutation sécurisée.
Quand choisir ArrayDeque plutôt que les alternatives
Arbre de décision :
- Besoin d'une
Queue? →ArrayDeque. Par défaut. - Besoin d'une pile ? →
ArrayDeque. Par défaut. Utilisezpush/pop/peek. - Besoin d'un
Deque? →ArrayDeque. Par défaut. - Besoin à la fois de sémantiques
DequeetListsur le même objet ? →LinkedList. Rare. - Besoin d'un ordre de priorité ? →
PriorityQueue. Abstraction différente. - Besoin d'un accès concurrent ? →
ConcurrentLinkedDeque(non borné) ouLinkedBlockingDeque(borné). Le bon choix dépend de si vous avez besoin de contre-pression.
La Javadoc l'énonce clairement en texte simple : « Cette classe est susceptible d'être plus rapide que Stack lorsqu'elle est utilisée comme pile, et plus rapide que LinkedList lorsqu'elle est utilisée comme file. » C'est écrit directement par les auteurs du JDK ; cela mérite d'être pris à la lettre.
Un exemple concret : file, pile, deque, fenêtre glissante
Le programme ci-dessous utilise une instance d'ArrayDeque comme file FIFO, une autre comme pile LIFO, et une troisième comme support d'un maximum de fenêtre glissante — un problème classique pour lequel ArrayDeque est la réponse de référence car on a besoin d'une mutation peu coûteuse aux deux extrémités.
Ce qu'on peut retenir de cette exécution :
- Les sections 1 et 2 utilisent la même classe pour deux usages distincts en appelant des méthodes différentes. File FIFO :
offerLast/pollFirst. Pile LIFO :push/pop. Il n'y a pas de classe séparée à apprendre. descendingIterator()est le parcours inverse économique — utile quand vous avez construit un deque comme une pile et souhaitez l'afficher du bas vers le haut.- La fonction de fenêtre glissante utilise les deux extrémités dans la même boucle —
pollFirstpour supprimer les indices sortis de la fenêtre,pollLastpour maintenir une pile décroissante monotone,peekFirstpour lire le maximum courant. Cet accès aux deux extrémités est la raison d'être d'ArrayDeque; tenter de faire cela avec unArrayListserait quadratique.
Et ensuite
Vous avez vu comment ArrayDeque implémente les deux extrémités de la relation file/pile. L'interface qu'il implémente depuis le début mérite son propre chapitre — Deque est le contrat qui rassemble tout ce que nous avons couvert pour les collections de type file. Nous aborderons cela à la prochaine session.