Java PriorityQueue
Usa la PriorityQueue basata su heap in Java per recuperare gli elementi in ordine di priorità, con ordinamento naturale o personalizzato.
PriorityQueue<E> è l'heap del JDK. È una Queue, ma invece del FIFO la testa è sempre l'elemento minimo secondo un Comparator (oppure l'ordine naturale se non ne viene fornito uno). offer è O(log n), poll e peek dalla testa sono rispettivamente O(log n) e O(1), e lo storage sottostante è un array piatto — nessuna allocazione di nodi per elemento. Questo la rende lo strumento giusto per problemi di tipo scheduler: "lavora sempre su ciò che è più urgente in questo momento."
Cosa significa "testa" in questo contesto
Per una coda FIFO, la testa è chi è arrivato per primo. Per una PriorityQueue, la testa è chi ha il valore più piccolo in quel momento — non il più piccolo al momento dell'inserimento:
peek()restituisce il minimo corrente.poll()rimuove il minimo corrente e ribilancia; il successivopeekmostra il nuovo minimo.offer(x)inserisce e ribilancia; sexè il nuovo minimo, il successivopeeklo restituisce.
"Minimo" è determinato da:
- Il
Comparatorpassato al costruttore, se presente. - Altrimenti, l'ordine naturale dell'elemento tramite
Comparable. Se gli elementi non sonoComparablee non è stato passato unComparator, la prima chiamata che necessita di un confronto lanciaClassCastException.
Non esiste una modalità max-heap. Se vuoi un max-heap, passa Comparator.reverseOrder() (o il tuo comparatore personalizzato invertito) — questo è l'idioma standard e lo usiamo nell'esempio qui sotto.
Costruirne una
// Natural order (Comparable elements).
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
// Reversed for max-heap behaviour.
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
// Custom comparator — by length, then alphabetically.
PriorityQueue<String> byLen = new PriorityQueue<>(
Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()));
// Pre-sized + comparator (initial capacity must come first).
PriorityQueue<String> bigByLen = new PriorityQueue<>(1_000,
Comparator.comparingInt(String::length));
// From an existing collection.
PriorityQueue<Integer> pq = new PriorityQueue<>(List.of(40, 10, 30, 20));Il costruttore con un singolo argomento che accetta una Collection è O(n) — esegue l'heapify in blocco invece di n offer in O(log n). Utile quando hai già tutti i dati a disposizione.
L'iterazione NON segue l'ordine di priorità
Questa è la sorpresa che coglie la maggior parte delle persone la prima volta. Iterare una PriorityQueue percorre l'array heap sottostante nell'ordine in cui l'heap memorizza i suoi nodi — non in ordine ascendente:
PriorityQueue<Integer> pq = new PriorityQueue<>(List.of(40, 10, 30, 20));
System.out.println(pq); // possibly [10, 20, 30, 40]
for (int x : pq) System.out.print(x + " "); // possibly: 10 20 30 40
// possibly: 10 40 30 20L'ordine di stampa è un dettaglio di implementazione del layout dell'heap. Se vuoi gli elementi in ordine di priorità, devi svuotare la coda:
while (!pq.isEmpty()) System.out.print(pq.poll() + " "); // sortedTienilo presente quando usi forEach, stream, toArray, o stampi semplicemente la coda per il debug. Lo Stream restituito da stream() non è ordinato, e aggiungere .sorted() richiede che gli elementi siano Comparable (oppure puoi passare un comparatore).
Mutare un elemento dopo offer rompe l'heap
La priorità viene calcolata al momento dell'inserimento. Se inserisci un elemento e poi muti il campo su cui si basa il comparatore, l'heap è ora in uno stato non valido — poll potrebbe restituire la testa sbagliata, contains potrebbe fallire, si perde l'invariante. Non esiste un metodo update o decrease-key.
Le due soluzioni alternative:
- Usa elementi immutabili — record con campi di priorità, o record wrapper come
record Task(int priority, String name) {}. Così non c'è nulla da mutare dopo l'inserimento. - Rimuovi e reinserisci se hai davvero bisogno di cambiare la priorità:
pq.remove(task)èO(n)(effettua una ricerca), poipq.offer(taskWithNewPriority)èO(log n).
Per una coda piccola, rimuovere-e-reinserire va bene. Per "sto riscrivendo l'algoritmo di Dijkstra e ho bisogno di un decrease-key veloce," una PriorityQueue non è lo strumento giusto — occorre un heap indicizzato o un TreeSet di coppie (priority, node).
null e thread safety
Stesse regole del resto di Queue:
- Nessun null.
pq.offer(null)lanciaNullPointerException. - Non thread-safe. L'accesso concorrente richiede
PriorityBlockingQueue, il cugino injava.util.concurrent.
Un esempio pratico: un piccolo scheduler di task
Il programma seguente usa una PriorityQueue per schedulare task in base alla priorità (numero più basso = più urgente). Mostra anche l'idioma del max-heap e la sorpresa sull'ordine di iterazione.
Interpretando l'esecuzione:
toString()eforEachhanno mostrato i task in un qualche ordine — non ordinato. Non usarli per il debug di "la priorità è corretta?" — svuota conpollper vedere l'ordine reale.- Il costruttore bulk ha prodotto un heap correttamente ordinato in una sola operazione — questo è il percorso di heapify in tempo lineare.
- Il blocco di mutazione alla fine è il punto critico in forma concentrata. Abbiamo mutato la priorità dell'array dopo averlo inserito, l'heap non ha avuto la possibilità di ribilanciarsi, e ora
peekmente. La soluzione è usare un wrapper immutabile oppure fareremove-e-offerdopo la modifica.
Cosa c'è dopo
Il prossimo capitolo tratta l'implementazione a cui ricorrere quando vuoi una coda FIFO o uno stack o una deque — e quella verso cui il Javadoc del JDK ti spinge come default per tutte e tre: ArrayDeque. È la classe che fa la maggior parte del lavoro dietro le quinte nel codice di esempio dei due capitoli precedenti.