Interfaccia Java Deque
Operazioni su coda a doppia estremità in Java con l'interfaccia Deque — addFirst, addLast, pollFirst, pollLast.
Deque<E> — pronunciato "deck," abbreviazione di double-ended queue — è Queue<E> con una seconda estremità. Mentre una Queue consente solo di rimuovere dalla testa, una Deque permette di inserire, rimuovere ed esaminare entrambe la testa e la coda. Questa singola capacità aggiuntiva è sufficiente per rendere Deque il contratto alla base di due astrazioni completamente diverse: è una coda, è uno stack, e il JDK la raccomanda come valore predefinito per entrambi.
Due estremità × tre operazioni × due stili di errore
L'interfaccia sembra intimidatoria a prima vista perché ogni operazione appare in dodici forme — testa vs coda, inserimento vs rimozione vs esame, lancia eccezione vs restituisce valore speciale. Ma è solo la griglia 3×2 di Queue estesa su due estremità:
| Operazione | Prima (testa) — lancia | Prima (testa) — valore speciale | Ultima (coda) — lancia | Ultima (coda) — valore speciale |
|---|---|---|---|---|
| Inserimento | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
| Rimozione | removeFirst() | pollFirst() | removeLast() | pollLast() |
| Esame | getFirst() | peekFirst() | getLast() | peekLast() |
La colonna "lancia" è quella da usare quando una deque vuota/piena in quel punto sarebbe un bug. La colonna "valore speciale" è quella da usare quando lo stato vuoto è atteso e si vuole verificarlo in una condizione di ciclo.
Il mapping della coda
Deque estende Queue, quindi una Deque è una Queue. I metodi ereditati sono alias per le varianti dalla testa alla coda:
Metodo Queue | Equivalente Deque |
|---|---|
add(e) / offer(e) | addLast(e) / offerLast(e) |
remove() / poll() | removeFirst() / pollFirst() |
element() / peek() | getFirst() / peekFirst() |
Inserimento dalla coda, rimozione dalla testa — questa è la disciplina FIFO che Queue già fornisce, solo espressa con i nomi espliciti dell'estremità.
Il mapping dello stack
Deque definisce anche tre metodi "stack" che agiscono tutti sulla testa:
| Metodo Stack | Equivalente Deque |
|---|---|
push(e) | addFirst(e) |
pop() | removeFirst() |
peek() | peekFirst() |
Push in entrata, pop in uscita — disciplina LIFO, stessa testa, estremità opposta rispetto al mapping di inserimento/rimozione della coda. Ecco perché il capitolo sullo Stack ti rimanda qui: il modo moderno di scrivere uno stack in Java è Deque<E> stack = new ArrayDeque<>(); e chiamare push / pop / peek.
La regola: null non è consentito
Come Queue, il contratto Deque riserva null come sentinel "vuoto" per pollFirst, pollLast, peekFirst, peekLast. Quindi ogni Deque di uso generale nel JDK rifiuta gli elementi null con NullPointerException all'inserimento. L'unica eccezione è LinkedList, che permette null per ragioni storiche — ed eredita tutta l'ambiguità che ne deriva. Non farlo.
Iterazione e iterazione inversa
Una Deque ha per convenzione due iteratori:
iterator()percorre dalla testa alla coda — l'ordine in cui si chiamerebbepollFirstripetutamente.descendingIterator()percorre dalla coda alla testa — l'ordine in cui si chiamerebbepollLastripetutamente.
Entrambi sono tipicamente fail-fast: mutare la deque dall'esterno dell'iteratore durante l'iterazione lancia ConcurrentModificationException. Usa Iterator.remove() o removeIf se devi mutare durante una scansione.
Scegliere un'implementazione
Nel codice single-threaded ci sono essenzialmente due scelte:
ArrayDeque— quella predefinita. Array circolare, O(1) su entrambe le estremità, nessuna allocazione di nodi per elemento, cache-friendly. Rifiutanull. ImplementaDequeeQueue.LinkedList— nodi doppiamente collegati. Implementa ancheList, quindi ogni elemento ha un nodo con puntatoriprev/next. Più lenta su entrambe le estremità rispetto adArrayDequee usa più memoria. Sceglila solo se hai effettivamente bisogno della semanticaDequeeListsullo stesso oggetto.
Per il codice concorrente (trattato più avanti nella parte sulla concorrenza del libro):
ConcurrentLinkedDeque— lock-free, illimitata.LinkedBlockingDeque— opzionalmente limitata, bloccante — la versione da usare per costruire una coda work-stealing o un producer/consumer con back-pressure su entrambe le estremità.
Un esempio pratico: coda, stack e verifica di palindromo in un'unica deque
Il programma seguente usa un ArrayDeque come coda FIFO, un altro come stack LIFO, e un terzo per verificare se una frase è un palindromo alimentando entrambe le estremità e confrontandole. Il punto è che la stessa interfaccia, persino la stessa classe, svolge tutti e tre i ruoli.
Ciò che questa esecuzione mostra:
- La stessa classe svolge sia il ruolo di coda che di stack senza rinominare un singolo metodo, se non per quale estremità si indirizza.
- I metodi "restituisce valore speciale" producono silenziosamente
nullper la deque vuota; i metodi "lancia eccezione" sollevanoNoSuchElementException. Scegli in base al fatto che lo stato vuoto sia atteso o un bug. - La verifica del palindromo usa entrambe le estremità nello stesso ciclo —
pollFirstepollLastinsieme. Questo è il pattern di accesso che solo unaDequefornisce in modo efficiente.
Cosa c'è dopo
Il capitolo su Deque chiude il lato delle code del framework. L'altra grande astrazione in Collection è quella che rifiuta i duplicati: un Set è una Collection con il contratto di unicità integrato. Lo affrontiamo nella prossima sezione.