Interface Queue en Java
Opérations FIFO en Java avec l'interface Queue — offer, poll, peek — et ses implémentations.
Queue<E> est une Collection<E> à laquelle s'ajoute la notion de tête. La tête est le prochain élément à être supprimé ; tous les autres attendent leur tour derrière elle. La sémantique par défaut — et celle qu'utilise presque chaque implémentation du JDK — est FIFO : premier entré, premier sorti. Les producteurs insèrent des éléments à la fin via offer ; les consommateurs les récupèrent en tête via poll. Ce chapitre porte sur le contrat : les six méthodes, les deux styles de gestion des erreurs, la règle sur les valeurs null, et quelle implémentation choisir selon la situation.
Deux façons de faire chaque chose — par conception
Chaque opération de file d'attente existe en deux variantes : l'une qui lève une exception en cas d'échec, et l'autre qui renvoie une valeur spéciale. La Javadoc présente cela sous forme d'une grille 3×2 :
| Opération | Lève une exception en cas d'échec | Renvoie une valeur spéciale |
|---|---|---|
| Insertion | boolean add(E e) (lève IllegalStateException si pleine) | boolean offer(E e) (renvoie false si pleine) |
| Suppression | E remove() (lève NoSuchElementException si vide) | E poll() (renvoie null si vide) |
| Examen | E element() (lève NoSuchElementException si vide) | E peek() (renvoie null si vide) |
La colonne « lève une exception » est celle héritée de Collection et List — elle est familière mais peu pratique quand le cas vide/plein est normal (un consommateur attendant la prochaine tâche, une file bornée rejetant un producteur en excès). La colonne « renvoie une valeur spéciale » a été ajoutée pour les files afin que vous puissiez écrire des boucles qui ne s'appuient pas sur des exceptions pour le contrôle de flux :
String item;
while ((item = queue.poll()) != null) {
process(item);
}Utilisez la forme retour de valeur par défaut. Recourez à remove/element uniquement lorsqu'une file vide à ce stade serait un véritable bug que vous voudriez signaler par une exception.
Null et Queue ne font pas bon ménage
Parce que poll() et peek() renvoient null pour signifier « vide », autoriser null comme élément réel est une source d'ambiguïté. Chaque Queue généraliste du JDK, à l'exception de LinkedList, rejette null — ArrayDeque, PriorityQueue, ArrayBlockingQueue, toutes les files concurrentes. LinkedList vous laisse ajouter un null, mais ce faisant vous avez rompu le contrat : il n'est plus possible de distinguer « la tête est null » de « la file est vide ».
La règle : ne stockez pas null dans une file. Préférez ArrayDeque à LinkedList et le langage l'imposera pour vous.
FIFO est la valeur par défaut — mais pas universelle
Queue n'impose pas FIFO. L'interface définit une tête et des opérations qui en extraient des éléments ; la façon dont la tête est déterminée dépend de l'implémentation. Les deux implémentations de java.util qui ne sont pas FIFO sont :
PriorityQueue— la tête est toujours l'élément le plus petit (selon l'ordre naturel ou unComparator). Les insertions se font là où le tas le dicte, pas en queue.- Les variantes bloquantes de
java.util.concurrent—PriorityBlockingQueue,DelayQueue, etc. — réordonnent par priorité, échéance, etc.
Tout le reste (ArrayDeque, LinkedList, ArrayBlockingQueue, LinkedBlockingQueue, ConcurrentLinkedQueue) est FIFO.
Choisir une implémentation
Pour le cas mono-thread :
ArrayDeque— le choix par défaut. Tableau circulaire, sans allocation par élément, rapide. Utilisez-le commeQueue<E>ou commeDeque<E>selon que vous avez besoin d'accéder aux deux extrémités.LinkedList— fonctionne, implémente égalementList. Choisissez-le uniquement si vous avez vraiment besoin des deux interfaces sur le même objet. Le chapitre sur LinkedList couvre les compromis.PriorityQueue— quand vous voulez « l'élément en attente le plus petit en premier » plutôt que FIFO.
Pour le cas multi-thread (couvert en détail dans la partie concurrence plus loin — indications seulement ici) :
ConcurrentLinkedQueue— FIFO sans verrou. Non borné.ArrayBlockingQueue— borné, sauvegardé par un tableau, bloqueputquand plein ettakequand vide. La file producteur/consommateur classique.LinkedBlockingQueue— optionnellement borné, version à nœuds liés du même principe.PriorityBlockingQueue—PriorityQueueconcurrent. Non borné.
La plupart du code applicatif utilise l'un des quatre premiers. Les files bloquantes sont la façon de construire des pools de workers et de la contre-pression.
Ce que vous pouvez appeler sur n'importe quelle Queue
Au-delà des six méthodes spécifiques à la tête, une Queue est aussi une Collection — donc size, isEmpty, contains, iterator, forEach, stream, clear, toArray fonctionnent toutes. L'ordre d'itération est l'ordre dans lequel les éléments seraient retirés par poll pour les implémentations FIFO (ArrayDeque, LinkedList), mais pour PriorityQueue l'ordre d'itération n'est pas l'ordre de priorité — il visite le tableau du tas sous-jacent tel quel. Si vous voulez les éléments dans l'ordre de priorité, vous devez vider la file avec poll jusqu'à ce qu'elle soit vide.
Exemple concret : producteur/consommateur en un seul thread
Le programme ci-dessous utilise un ArrayDeque comme file FIFO pour modéliser un petit système de gestion d'impressions : les travaux arrivent au fil du temps (nous le représentons en insérant par lots via offer), et le worker vide chaque lot avec poll. La paire de styles d'erreur est montrée en haut afin que vous puissiez voir exactement quand chaque forme se déclenche.
Quelques observations à tirer de l'exécution :
polletpeekont renvoyé silencieusementnulllorsque la file était vide ;removeetelementont levé des exceptions. Les deux comportements font partie du contrat — choisissez celui qui correspond à votre cas : « vide » est-il une erreur ou simplement un état ?offer(null)a levé une exception surArrayDeque. C'est l'implémentation qui fait respecter la règle dont dépend l'interface.- La
PriorityQueuea renvoyé10, 20, 30, 40parpoll— dans l'ordre trié, pas dans l'ordre d'insertion. Même interfaceQueue, règle de sélection de la tête totalement différente.
Ce qui vient ensuite
La première implémentation non-FIFO mérite son propre chapitre — PriorityQueue est une file sauvegardée par un tas où la tête est toujours l'élément le plus petit. C'est l'ingrédient de base de presque tous les ordonnanceurs « traiter la tâche la plus urgente en premier » du JDK.