So far the collections in this module answered two questions: "is it there?" (Set, Map) and "where is it?" (List). Queues answer a third one, the one that governs any system processing pending work: "whose turn is it now?".
That question turns up everywhere. The reservation queue for a material in BiblioTech. The pending jobs of a print server. The packets arriving at a network card. The tasks of a scheduler. The nodes still to visit in a breadth-first traversal. The due-date notices sorted by urgency. In all those cases there are elements that come in, wait and go out following a specific policy, and the data structure that models that policy is a queue.
In this lesson you will see the Queue interface with its FIFO semantics, its curious duplication of methods —two ways of doing the same thing with different behaviour on failure—, the Deque interface that opens both ends, ArrayDeque with its circular buffer and its status as today's default option, and the PriorityQueue, which serves the most urgent element instead of the oldest and hides a surprise that catches many people out. By the end, BiblioTech will have its reservation queue rewritten properly and a priority queue for the delay notices.
Contents
- FIFO: the semantics of a queue
- The
Queueinterface and its two families of methods ArrayDeque: the circular bufferDeque: the double-ended queueArrayDequeversusLinkedListPriorityQueue: serving the most urgent- The surprise of
PriorityQueue's iterator BlockingQueueand the producer-consumer pattern- Real use cases: buffers, scheduling and BFS
- Applying it to BiblioTech
- Common Mistakes and Tips
- Exercises
- FIFO: the semantics of a queue
A queue is a collection with a defined exit policy. The classic policy is FIFO: First In, First Out, "the first to come in is the first to go out". Exactly like the queue at the supermarket.
flowchart LR
E["in<br/>(offer / addLast)"] --> C4["Nuria"]
C4 --> C3["Diego"]
C3 --> C2["Marta"]
C2 --> S["out<br/>(poll / removeFirst)"]
Elements come in at the tail (the back) and go out at the head (the front). The first to arrive is the first to be served: it is a fair policy, guaranteeing nobody waits indefinitely.
Compare it with the other two policies you will see in this module:
| Policy | Who goes out first | Structure | Lesson |
|---|---|---|---|
| FIFO | The one who has been waiting longest | Queue / Deque |
This one |
| LIFO | The last one in | Stack (Deque) |
05-08 |
| By priority | The most urgent, no matter when it arrived | PriorityQueue |
This one, section 6 |
The essential difference from a List is that a queue offers no arbitrary access. There is no get(i). You can only look at the first one and take out the first one. That restriction is not a shortcoming: it is the guarantee that the processing order is respected. If the code could jump into the middle of the queue, the policy would no longer be guaranteed by the structure.
import java.util.ArrayDeque;
import java.util.Queue;
Queue<String> pending = new ArrayDeque<>();
pending.offer("Marta Ruiz"); // comes in
pending.offer("Diego Alonso");
pending.offer("Nuria Vidal");
System.out.println(pending.peek()); // Marta Ruiz (looks without taking)
System.out.println(pending.poll()); // Marta Ruiz (takes)
System.out.println(pending.poll()); // Diego Alonso
System.out.println(pending.size()); // 1
- The
Queue interface and its two families of methods
Queue interface and its two families of methodsQueue extends Collection and declares six methods, which are really three operations duplicated:
| Operation | Throws an exception on failure | Returns a special value |
|---|---|---|
| Insert | add(e) → IllegalStateException |
offer(e) → false |
| Extract | remove() → NoSuchElementException |
poll() → null |
| Inspect | element() → NoSuchElementException |
peek() → null |
Why are there two versions of each?
Because there are two different situations and they deserve different treatment.
The family that throws an exception (add, remove, element) considers failure an anomaly. If you expect the queue to have elements and it does not, something is wrong in your logic and you want to know immediately.
The family that returns a special value (offer, poll, peek) considers failure a normal situation. An empty queue is not an error: it means there is no pending work.
Queue<Reservation> queue = new ArrayDeque<>();
// A processing loop: the queue emptying is NORMAL, it is the stopping condition
Reservation r;
while ((r = queue.poll()) != null) { // poll returns null: idiomatic and clean
serve(r);
}
// A one-off query where empty WOULD BE a logic error
Reservation next = queue.remove(); // NoSuchElementException if it is emptyBesides, offer makes sense on bounded queues (with a maximum capacity), where inserting can legitimately fail because there is no room. ArrayDeque and LinkedList have no limit, so their offer always returns true; but the BlockingQueues of module 8 do have one, and there the difference matters a great deal.
The practical rule
| Situation | Use |
|---|---|
| A loop consuming until the queue is empty | poll() |
| Checking whether there is pending work | peek() |
| Adding to an unbounded queue | offer() or add(), either way |
| Adding to a bounded queue | offer(), and look at the result |
| An empty queue indicates a logic failure | remove() / element() |
General recommendation: use offer, poll and peek. They avoid exceptions for normal situations and make the code more robust. The exception variants are useful when you want an unexpected state to show itself instead of propagating a silent null.
A warning about peek and poll: if the queue allowed null elements, you could not tell "it is empty" from "the first element is null". That is exactly why ArrayDeque and PriorityQueue forbid nulls, as you will see shortly.
ArrayDeque: the circular buffer
ArrayDeque: the circular bufferArrayDeque is the default implementation of queues and stacks in modern Java. Inside it is a circular array, and understanding that idea explains why it is so fast.
The problem it solves is this: in an ArrayList, taking out the first element forces every other one to be shifted, O(n). How do you avoid that without using linked nodes? The answer: do not move the elements; move the indices.
ArrayDeque keeps an array and two indices, head and tail. Extracting at the head means incrementing head. Inserting at the back means writing at tail and incrementing it. Nobody moves.
flowchart TB
subgraph state1["Initial state: head=0, tail=3"]
direction LR
A0["[0] Marta"]
A1["[1] Diego"]
A2["[2] Nuria"]
A3["[3] -"]
A4["[4] -"]
A5["[5] -"]
end
subgraph state2["After 2 polls: head=2, tail=3"]
direction LR
B0["[0] -"]
B1["[1] -"]
B2["[2] Nuria"]
B3["[3] -"]
B4["[4] -"]
B5["[5] -"]
end
subgraph state3["After 4 offers: tail WRAPS AROUND to 1"]
direction LR
C0["[0] Ana"]
C1["[1] -"]
C2["[2] Nuria"]
C3["[3] Luis"]
C4["[4] Eva"]
C5["[5] Pau"]
end
state1 --> state2 --> state3
When tail reaches the end of the array, it wraps around to the beginning, as long as there is free space there. Hence the name "circular": the array behaves like a ring. The computation is as simple as in HashMap:
And for the same reason as in HashMap, the capacity is always a power of two: it lets the modulo be replaced by a bitwise AND.
When the array fills up, it is doubled and the elements are copied, with the same amortised-cost reasoning as in ArrayList (05-03).
The result is these complexities:
| Operation | ArrayDeque |
|---|---|
offerFirst / offerLast |
amortised O(1) |
pollFirst / pollLast |
O(1) |
peekFirst / peekLast |
O(1) |
contains(o) |
O(n) |
remove(Object) |
O(n) |
size() |
O(1) |
And its two restrictions, both deliberate:
It allows no null. Because it uses null internally as an empty-cell marker, and because poll() and peek() return null to mean "empty queue". Allowing null elements would make that signal ambiguous. Trying to insert one throws NullPointerException.
It is not synchronised. For concurrent use there are the BlockingQueues and ConcurrentLinkedDeque of module 8.
Deque: the double-ended queue
Deque: the double-ended queueDeque (pronounced "deck", from Double Ended QUEue) extends Queue and allows inserting, extracting and inspecting at both ends. It is the most versatile interface in the Framework: it serves as a FIFO queue and as a LIFO stack.
flowchart LR
IF["addFirst<br/>offerFirst"] --> D["DEQUE"]
D --> RF["removeFirst / pollFirst<br/>getFirst / peekFirst"]
IL["addLast<br/>offerLast"] --> D
D --> RL["removeLast / pollLast<br/>getLast / peekLast"]
The complete table of its twelve main methods:
| Operation | End | Throws an exception | Special value |
|---|---|---|---|
| Insert | Head | addFirst(e) |
offerFirst(e) |
| Insert | Tail | addLast(e) |
offerLast(e) |
| Extract | Head | removeFirst() |
pollFirst() |
| Extract | Tail | removeLast() |
pollLast() |
| Inspect | Head | getFirst() |
peekFirst() |
| Inspect | Tail | getLast() |
peekLast() |
And it also inherits Queue's methods and adds the stack ones, which are aliases of the previous ones:
| Inherited method or alias | Equivalent to | Semantics |
|---|---|---|
add(e) / offer(e) |
addLast(e) / offerLast(e) |
Queue |
remove() / poll() |
removeFirst() / pollFirst() |
Queue |
element() / peek() |
getFirst() / peekFirst() |
Queue |
push(e) |
addFirst(e) |
Stack |
pop() |
removeFirst() |
Stack |
The three modes of use, on the same class:
// As a QUEUE (FIFO): in at the back, out at the front
Deque<String> queue = new ArrayDeque<>();
queue.offerLast("Marta Ruiz");
queue.offerLast("Diego Alonso");
System.out.println(queue.pollFirst()); // Marta Ruiz
// As a STACK (LIFO): in and out at the same end
Deque<String> stack = new ArrayDeque<>();
stack.push("registration");
stack.push("withdrawal");
System.out.println(stack.pop()); // withdrawal (the last one in)
// As a real DEQUE: both ends
Deque<String> both = new ArrayDeque<>();
both.offerFirst("urgent"); // jumps to the front
both.offerLast("normal"); // waits its turn at the back
System.out.println(both.pollFirst()); // urgentThat third mode is the one that gives the structure its name and solves real cases: a work queue where urgent tasks are inserted at the front, a history that grows at one end and is trimmed at the other, an algorithm that needs to look at and consume from both ends.
Deque also offers descendingIterator(), which walks from the tail to the head, and removeFirstOccurrence/removeLastOccurrence to remove specific appearances.
And an important warning: a Deque is not a List. It has no get(i), set(i, e) or indexOf. If you need access by index, you did not want a Deque.
ArrayDeque versus LinkedList
ArrayDeque versus LinkedListBoth implement Deque. The comparison closes the discussion opened in 05-04:
| Aspect | ArrayDeque |
LinkedList |
|---|---|---|
| Internal structure | Circular array | Doubly linked nodes |
| Memory per element | ~4-8 bytes (one reference) | ~28 bytes (a Node object) |
| Cache locality | Excellent (contiguous) | Poor (scattered) |
offerFirst / offerLast |
amortised O(1) | O(1) |
pollFirst / pollLast |
O(1) | O(1) |
| Traversal | Fast | 2-10× slower |
| Pressure on the collector | A single array | One object per element |
Allows null |
No | Yes |
Implements List |
No | Yes |
| Official recommendation | Yes, as a queue and a stack | Only if you need List + Deque |
The JDK's own documentation is explicit: "this class [ArrayDeque] is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue".
Choose ArrayDeque unless you need null or the List interface in the same variable. There are no other cases.
And the practical conclusion we have been building since 05-03:
| I need... | Implementation |
|---|---|
| A list | ArrayList |
| A set | HashSet |
| A map | HashMap |
| A queue or a stack | ArrayDeque |
| A priority queue | PriorityQueue |
PriorityQueue: serving the most urgent
PriorityQueue: serving the most urgentA PriorityQueue breaks FIFO: it does not serve whoever has been waiting longest, but the highest priority one. It is the structure of an emergency department, of a task scheduler or —in BiblioTech— of a notice list where the first person called is the one with the most days overdue.
import java.util.PriorityQueue;
import java.util.Queue;
// By natural order: the SMALLEST comes out first
Queue<Integer> queue = new PriorityQueue<>();
queue.offer(30);
queue.offer(10);
queue.offer(20);
System.out.println(queue.poll()); // 10
System.out.println(queue.poll()); // 20
System.out.println(queue.poll()); // 30By default it uses the natural order (Comparable) and serves the smallest first. With a Comparator you can define any criterion, picking up all of 04-06:
// The loans with the MOST days overdue first
Queue<Loan> notices = new PriorityQueue<>(
Comparator.comparingInt((Loan l) -> l.daysLate(currentDay)).reversed());
// The most urgent reservations first and, on a tie, the oldest
Queue<Reservation> reservations = new PriorityQueue<>(
Comparator.comparingInt(Reservation::getPriority)
.thenComparingInt(Reservation::getRequestDay));How it works: the binary heap
A PriorityQueue does not keep every element sorted —that would cost too much—. It uses a binary heap, an almost complete binary tree with a single rule, called the heap property:
Every node is smaller than or equal to its children.
From that it follows that the root is always the minimum, which is the only thing we need in order to know whose turn it is.
flowchart TB
R["10<br/>(root = minimum)"]
A["20"]
B["15"]
C["40"]
D["25"]
E["30"]
R --> A
R --> B
A --> C
A --> D
B --> E
Notice that the order is not total: 15 is to the right of 20, even though it is smaller. The only guarantee is the parent-child relationship, and that is enough.
The heap is stored in an array, with no nodes and no references: the left child of index i is at 2i+1 and the right one at 2i+2. That gives it excellent cache locality.
The operations work like this:
offer(e): the element is placed at the end of the array and made to "float up" by swapping it with its parent while it is smaller. Since the tree has height log n, the cost is O(log n).poll(): the root (the minimum) is taken, the last element is put in its place and made to "sink down" by swapping it with the smaller of its children. Also O(log n).peek(): it isarray[0]. O(1).
| Operation | PriorityQueue |
|---|---|
offer(e) |
O(log n) |
poll() |
O(log n) |
peek() |
O(1) |
contains(o) |
O(n) |
remove(Object) |
O(n) |
| Traversing in priority order | O(n log n) by emptying it with poll |
And its restrictions:
- It allows no
null: it cannot be compared withnull. - It requires
Comparableor aComparator. Without either, the first insertion throwsClassCastException. - It is not stable: two elements with the same priority come out in arbitrary order. If that matters to you, add a tie-breaking criterion to the comparator (for example, order of arrival).
- The surprise of
PriorityQueue's iterator
PriorityQueue's iteratorThis is the detail that catches almost everybody out the first time:
Queue<Integer> queue = new PriorityQueue<>();
queue.offer(30);
queue.offer(10);
queue.offer(20);
queue.offer(5);
System.out.println(queue); // [5, 10, 20, 30] or [5, 10, 20, 30]... it depends
for (int n : queue) {
System.out.print(n + " "); // 5 10 20 30 ... or NOT
}A PriorityQueue's iterator does not traverse in priority order. It walks the internal array exactly as it stands, and that array only satisfies the heap property, not a total order. The same goes for toString(), for forEach and for toArray().
With this data:
Queue<Integer> queue = new PriorityQueue<>();
for (int n : new int[]{ 50, 40, 30, 20, 10 }) { queue.offer(n); }
System.out.println("toString: " + queue); // [10, 20, 40, 50, 30] <- NOT sorted
System.out.print("iterator: ");
queue.forEach(n -> System.out.print(n + " ")); // 10 20 40 50 30
System.out.print("\npoll: ");
while (!queue.isEmpty()) { System.out.print(queue.poll() + " "); } // 10 20 30 40 50The only guarantee is that peek() and poll() return the highest-priority element. Everything else is the heap's internal state.
How to traverse it correctly
By consuming it with poll:
while (!queue.isEmpty()) {
Loan l = queue.poll(); // in guaranteed priority order
process(l);
}
// ...but the queue is left emptyOn a copy, if you need to keep it:
Queue<Loan> copy = new PriorityQueue<>(queue); // copies the heap
while (!copy.isEmpty()) {
process(copy.poll());
}
// the original queue is left intactBy dumping into a list and sorting it, if you are going to traverse several times:
List<Loan> sorted = new ArrayList<>(queue);
sorted.sort(queue.comparator()); // the same criterion as the queueAnd an alternative worth considering: if what you need is a collection that is always sorted and traversable in order, TreeSet (05-06) is a better option than PriorityQueue. The PriorityQueue is optimised for "give me the next one", not for "walk all of me".
| I need | Structure |
|---|---|
| To always extract the highest priority one | PriorityQueue |
| To always traverse in order, and query ranges | TreeSet |
| To sort once, at the end | List + sort (05-09) |
BlockingQueue and the producer-consumer pattern
BlockingQueue and the producer-consumer patternThere is a family of queues designed for several threads to communicate: the BlockingQueues (ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue). Their distinctive trait is two operations that wait:
take(): if the queue is empty, the thread blocks until somebody inserts something.put(e): if the queue is full, the thread blocks until somebody takes something out.
On top of them the most classic concurrency pattern there is gets built, the producer-consumer:
flowchart LR
P1["Producer 1"] --> Q["BlockingQueue<br/>(shared queue)"]
P2["Producer 2"] --> Q
Q --> C1["Consumer 1"]
Q --> C2["Consumer 2"]
One or more threads produce work and deposit it in the queue; one or more consume it. The queue acts as a buffer: it absorbs production peaks and decouples the pace on both sides, so that neither has to know the other or wait actively.
Its advantages are clear: producers do not worry about whether there are free consumers, consumers do not continually poll for work (the queue puts them to sleep and wakes them), and the queue's maximum capacity acts as natural flow control: if the consumers cannot keep up, the queue fills and the producers slow down by themselves.
In BiblioTech, a natural case would be an overnight process that computes fines: one thread walks the loans and deposits them in the queue, and several threads compute and print the notices.
The real implementation of all this is module 8, which covers threads, synchronisation, ExecutorService and concurrent collections. Here you only need to know that it exists, that it rests on the same Queue interface you have just learned, and that you must never share an ArrayDeque or a PriorityQueue between threads without synchronisation: they are not thread-safe.
- Real use cases: buffers, scheduling and BFS
Buffers
A queue between two processes of different speeds absorbs the differences in pace. A fast reader deposits lines in the queue and a slow processor consumes them, without either waiting for the other more than necessary. It is the basis of streams with BufferedReader (module 7) and of messaging queues.
A useful variant is the bounded circular buffer, which discards the oldest item when it fills up:
Deque<String> lastOperations = new ArrayDeque<>();
void log(String operation) {
lastOperations.offerLast(operation);
if (lastOperations.size() > 100) {
lastOperations.pollFirst(); // O(1): discards the oldest
}
}With an ArrayList this would be O(n) per entry; with a Deque it is O(1).
Scheduling
A queue of pending tasks:
Queue<Runnable> tasks = new ArrayDeque<>();
tasks.offer(() -> System.out.println("Calculate fines"));
tasks.offer(() -> System.out.println("Send notices"));
tasks.offer(() -> System.out.println("Generate report"));
Runnable task;
while ((task = tasks.poll()) != null) {
task.run(); // they run in order of arrival
}If there are urgent items as well, a PriorityQueue with a comparator by priority.
Breadth-first search (BFS)
This is the most important algorithmic use of queues. Breadth-first search explores a structure level by level: first the direct neighbours, then the neighbours of the neighbours, and so on. It guarantees finding the shortest path in number of steps.
The scheme is always the same: a queue of nodes still to visit and a set of already visited ones (a Set, from 05-06, so as not to repeat or fall into cycles).
A small example in BiblioTech: related materials ("people who read this also read that"), and we want the recommendations sorted by closeness.
package com.nexussoftware.bibliotech.service;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Recommender {
/** Graph: reference -> related references. */
private final Map<String, List<String>> related;
public Recommender(Map<String, List<String>> related) {
this.related = related;
}
/**
* Breadth-first traversal: returns the references reachable from 'origin'
* up to 'maxDepth' hops away, in order of CLOSENESS.
*/
public List<String> recommend(String origin, int maxDepth) {
List<String> result = new ArrayList<>();
Set<String> visited = new HashSet<>();
Deque<String> toVisit = new ArrayDeque<>();
Deque<Integer> depths = new ArrayDeque<>();
toVisit.offerLast(origin);
depths.offerLast(0);
visited.add(origin);
while (!toVisit.isEmpty()) {
String current = toVisit.pollFirst(); // FIFO: level by level
int depth = depths.pollFirst();
if (depth > 0) { result.add(current); } // the origin is not recommended
if (depth >= maxDepth) { continue; }
for (String neighbour : related.getOrDefault(current, List.of())) {
if (visited.add(neighbour)) { // add returns false if it was there (05-06)
toVisit.offerLast(neighbour);
depths.offerLast(depth + 1);
}
}
}
return result;
}
}Usage:
Map<String, List<String>> graph = Map.of(
"978-0000000001", List.of("978-0000000002", "978-0000000003"), // Effective Java
"978-0000000002", List.of("978-0000000003", "DVD-0007"), // Design Patterns
"978-0000000003", List.of("DVD-0007"), // Refactoring
"DVD-0007", List.of("REV-2024-03")
);
Recommender r = new Recommender(graph);
System.out.println(r.recommend("978-0000000001", 1)); // direct neighbours
System.out.println(r.recommend("978-0000000001", 3)); // up to 3 hops, by closenessThe fact that the queue is FIFO is what guarantees the level-by-level order: every node at distance 1 is processed before any at distance 2. If you swapped the queue for a stack (push/pop), the same code would do a depth-first traversal, with completely different results. You will see that comparison in 05-08.
- Applying it to BiblioTech
The reservation queue, now with a Deque
In 05-04 you wrote ReservationQueue on top of LinkedList and it was already announced that ArrayDeque would be better. Here is the definitive version:
package com.nexussoftware.bibliotech.service;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import com.nexussoftware.bibliotech.domain.Employee;
import com.nexussoftware.bibliotech.domain.Material;
import com.nexussoftware.bibliotech.domain.Reservation;
/**
* FIFO queue of reservations for ONE material, on top of ArrayDeque.
*
* Compared with the LinkedList version of 05-04: the same O(1) at the ends,
* but with a circular array instead of nodes -> less memory, better cache
* locality and no pressure at all on the collector.
*/
public class ReservationQueue {
private final Material material;
private final Deque<Reservation> pending = new ArrayDeque<>();
public ReservationQueue(Material material) {
this.material = material;
}
/** Enqueue at the back: amortised O(1). */
public void reserve(Employee employee, int day) {
pending.offerLast(new Reservation(employee, material, day));
}
/** An urgent reservation: it jumps to the front. Only a Deque allows this. */
public void reserveUrgent(Employee employee, int day) {
pending.offerFirst(new Reservation(employee, material, day, 1));
}
/** peekFirst: null if there is nobody. It never throws an exception. */
public Reservation next() {
return pending.peekFirst();
}
/** pollFirst: serves and removes. O(1). */
public Reservation serveNext(int day) {
Reservation r = pending.pollFirst();
if (r != null) {
r.markFulfilled();
material.lend();
}
return r;
}
/** Cancels an employee's most recent reservation, walking from the back. */
public boolean cancelLatestOf(Employee employee) {
Iterator<Reservation> it = pending.descendingIterator();
while (it.hasNext()) {
if (it.next().getEmployee().equals(employee)) {
it.remove();
return true;
}
}
return false;
}
/** Expires the reservations that have been waiting too long. */
public int expire(int currentDay, int maxDays) {
int before = pending.size();
pending.removeIf(r -> r.daysWaiting(currentDay) > maxDays);
return before - pending.size();
}
/** Position in the queue (1 = next up). 0 if there is no reservation. */
public int positionOf(Employee employee) {
int position = 1;
for (Reservation r : pending) { // a Deque has NO get(i): always for-each
if (r.getEmployee().equals(employee)) { return position; }
position++;
}
return 0;
}
public List<Reservation> list() { return new ArrayList<>(pending); }
public int waiting() { return pending.size(); }
public boolean hasPending() { return !pending.isEmpty(); }
}The priority notice queue
And now a PriorityQueue that always serves the loan with the most days overdue:
package com.nexussoftware.bibliotech.service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
import com.nexussoftware.bibliotech.domain.Severity;
import com.nexussoftware.bibliotech.domain.Loan;
/** Due-date notices served by urgency, not by order of arrival. */
public class NoticeQueue {
private final Queue<Loan> notices;
private final int currentDay;
public NoticeQueue(int currentDay) {
this.currentDay = currentDay;
// Most days overdue first; on a tie, the oldest loan.
// The tie-breaker makes the order DETERMINISTIC: without it, two loans
// with the same delay would come out in arbitrary order.
this.notices = new PriorityQueue<>(
Comparator.comparingInt((Loan l) -> daysLate(l)).reversed()
.thenComparingInt(Loan::getLoanDay));
}
private int daysLate(Loan l) {
return l.getMaterial().calculateDaysLate(currentDay - l.getLoanDay());
}
/** Enqueues only what is really overdue. O(log n). */
public void enqueue(Loan l) {
if (l != null && !l.isReturned() && l.isOverdue(currentDay)) {
notices.offer(l);
}
}
public void enqueueAll(List<Loan> loans) {
for (Loan l : loans) { enqueue(l); }
}
/** The most urgent one, without taking it out. O(1). */
public Loan mostUrgent() {
return notices.peek();
}
/**
* Processes ALL the notices in order of urgency.
*
* IMPORTANT: the queue has to be emptied with poll(). Walking it with a
* for-each or printing its toString() would give the internal heap's order,
* which is NOT the priority order.
*/
public int processAll() {
int processed = 0;
Loan l;
while ((l = notices.poll()) != null) { // the idiom from section 2
int late = daysLate(l);
Severity s = l.getMaterial().classifySeverity(currentDay - l.getLoanDay());
System.out.printf("[%-10s] %-16s %-24s %3d days %6.2f EUR%n",
s, l.getEmployee().getName(), l.getMaterial().getTitle(),
late, l.calculateFine(currentDay));
processed++;
}
return processed;
}
/** The N most urgent ones, WITHOUT emptying the queue: we work on a copy. */
public List<Loan> topUrgent(int howMany) {
Queue<Loan> copy = new PriorityQueue<>(notices); // copies the heap
List<Loan> result = new ArrayList<>();
for (int i = 0; i < howMany && !copy.isEmpty(); i++) {
result.add(copy.poll());
}
return result;
}
public int pending() { return notices.size(); }
}Both together in use:
Employee marta = new Employee("Marta Ruiz", "EMP-001");
Employee diego = new Employee("Diego Alonso", "EMP-002");
Employee nuria = new Employee("Nuria Vidal", "EMP-003");
Material effectiveJava = new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018);
// --- Reservation queue (FIFO) ---
effectiveJava.lend();
ReservationQueue reservations = new ReservationQueue(effectiveJava);
reservations.reserve(marta, 100);
reservations.reserve(diego, 101);
reservations.reserveUrgent(nuria, 102); // jumps to the front
System.out.println("Next: " + reservations.next().getEmployee().getName());
System.out.println("Position of Marta: " + reservations.positionOf(marta));
// --- Notice queue (by priority) ---
List<Loan> loans = List.of(
new Loan(new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018), marta, 100),
new Loan(new Magazine("Java Magazine", "REV-2024-03", 42, "Monthly"), diego, 120),
new Loan(new Dvd("Refactoring Live", "DVD-0007", 95), nuria, 125),
new Loan(new Book("Design Patterns", "Erich Gamma", "978-0000000002", 1994), diego, 110)
);
NoticeQueue notices = new NoticeQueue(140);
notices.enqueueAll(loans);
System.out.println("\nPending notices: " + notices.pending());
System.out.println("Most urgent: " + notices.mostUrgent().getMaterial().getTitle());
System.out.println("\n--- Processing by urgency ---");
notices.processAll();Next: Nuria Vidal Position of Marta: 2 Pending notices: 4 Most urgent: Java Magazine --- Processing by urgency --- [SEVERE ] Diego Alonso Java Magazine 13 days 1.30 EUR [SEVERE ] Nuria Vidal Refactoring Live 12 days 6.00 EUR [SEVERE ] Marta Ruiz Effective Java 25 days 6.25 EUR [SEVERE ] Diego Alonso Design Patterns 15 days 3.75 EUR
Notice that the order is not the insertion order or the loan-day order: it is descending days overdue, computed according to each material type's own term. A magazine 13 days overdue is more urgent than a book 25 days overdue, because a magazine's term is 7 days and a book's is 15. The PriorityQueue applies that logic without the processing code having to know anything about it.
Common Mistakes and Tips
Walking a PriorityQueue with a for-each expecting priority order. It does not give it: it walks the internal heap. The same goes for toString(), forEach and toArray(). The only way is to empty it with poll(), or dump it into a list and sort it.
Inserting null into an ArrayDeque or a PriorityQueue. NullPointerException. It is deliberate: poll() and peek() use null to signal "empty". If you need to store absences, rethink the model or use Optional (10-04).
Confusing remove() with poll(). On an empty queue, remove() throws NoSuchElementException and poll() returns null. Choose according to whether an empty queue is normal or is an error.
Using LinkedList as a queue. It works, but ArrayDeque is faster and uses far less memory. The official recommendation is ArrayDeque.
Looking for get(i) in a Deque. It does not exist: a Deque is not a List. If you need access by index, you chose the wrong structure.
Using contains or remove(Object) on a queue inside a loop. Both are O(n) on ArrayDeque and PriorityQueue. If you need to search often, also keep a supporting Set or Map.
A Comparator with no tie-breaker in a PriorityQueue. You will not lose elements —that only happens in a TreeSet (05-06)— but the order among ties will be arbitrary and not reproducible between runs. Add a tie-breaking criterion if the order has to be deterministic.
Modifying an element already enqueued in a PriorityQueue. If you change the field it is ordered by, the heap is not reorganised: the element is left in the wrong position and the exit order stops being reliable. Take it out, modify it and enqueue it again.
Sharing an ArrayDeque or PriorityQueue between threads. They are not thread-safe. That is what the BlockingQueues and ConcurrentLinkedQueue of module 8 are for.
Tip: while ((x = queue.poll()) != null) is the standard idiom for emptying a queue. It is more compact and safer than combining isEmpty() with remove().
Tip: declare by the interface that reflects the use. Queue<X> if you only consume FIFO, Deque<X> if you use both ends or it is a stack. That way the type documents the intent.
Exercises
Exercise 1: a loan waiting room
Write WaitingRoom to manage the serving of employees at BiblioTech's desk, using a Deque<Employee>:
void arrive(Employee e): goes to the back.void arrivePriority(Employee e): goes to the front (management staff).Employee serve(): serves the first one,nullif there is nobody.Employee next(): inspects without serving.boolean leave(Employee e): the employee leaves the queue from wherever they are.int positionOf(Employee e).List<Employee> reverseOrder(): from the last to the first, withdescendingIterator.void closeDesk(): serves everybody in order, printing each service.
Document in comments which operations are O(1) and which are O(n).
Exercise 2: a task scheduler with priority
Create a record MaintenanceTask(String description, int priority, int creationDay) and write TaskScheduler with a PriorityQueue<MaintenanceTask> where the one with the lowest priority number comes out first (1 = maximum) and, on a tie, the oldest:
void schedule(MaintenanceTask t).MaintenanceTask next()without extracting.MaintenanceTask execute()extracting.List<MaintenanceTask> upcoming(int howMany): the next N without emptying the queue.int executeAll(): executes them all in order, printing.void demoUnorderedIterator(): prints the queue withtoString, with afor-eachand then by emptying it withpoll, showing that only the last one is in order.
Exercise 3: breadth-first versus depth-first
Extend the Recommender from section 9 with a method recommendDepthFirst(String origin, int maxDepth) that uses a stack (push/pop on an ArrayDeque) instead of a queue, leaving the rest of the algorithm identical.
Write a main that runs both on the same graph and explains in comments why the results differ and in what situations each one is preferable.
Solutions
Solution 1
package com.nexussoftware.bibliotech.service;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import com.nexussoftware.bibliotech.domain.Employee;
/** Waiting room at the loans desk. */
public class WaitingRoom {
private final Deque<Employee> queue = new ArrayDeque<>();
/** Amortised O(1): write at 'tail' and increment it. */
public void arrive(Employee e) {
if (e != null) { queue.offerLast(e); }
}
/**
* O(1): write at 'head-1' and decrement it (the array is CIRCULAR,
* so 'head' can wrap around to the end of the array).
* With an ArrayList this would be add(0, e): O(n).
*/
public void arrivePriority(Employee e) {
if (e != null) { queue.offerFirst(e); }
}
/** O(1). pollFirst returns null if it is empty; removeFirst would throw. */
public Employee serve() {
return queue.pollFirst();
}
/** O(1). */
public Employee next() {
return queue.peekFirst();
}
/**
* O(n): the queue has to be walked looking for the employee. It is the only
* expensive operation in this class, and it is acceptable because leaving
* from the middle of the queue is exceptional.
*/
public boolean leave(Employee e) {
return queue.removeFirstOccurrence(e); // uses equals (03-09)
}
/** O(n). A Deque has no get(i): the position is only known by walking. */
public int positionOf(Employee e) {
int position = 1;
for (Employee current : queue) {
if (current.equals(e)) { return position; }
position++;
}
return 0;
}
/** O(n). descendingIterator walks from 'tail' to 'head'. */
public List<Employee> reverseOrder() {
List<Employee> result = new ArrayList<>(queue.size());
Iterator<Employee> it = queue.descendingIterator();
while (it.hasNext()) { result.add(it.next()); }
return result;
}
/** Empties the queue serving in order. The standard idiom with poll. */
public void closeDesk() {
System.out.println("--- Closing the desk: " + queue.size() + " waiting ---");
Employee e;
int turn = 1;
while ((e = queue.pollFirst()) != null) {
System.out.printf(" Turn %d: %s (%s)%n",
turn++, e.getName(), e.getIdentifier());
}
System.out.println("--- Desk closed ---");
}
public int waiting() { return queue.size(); }
public boolean isEmpty() { return queue.isEmpty(); }
}A test:
Employee marta = new Employee("Marta Ruiz", "EMP-001");
Employee diego = new Employee("Diego Alonso", "EMP-002");
Employee nuria = new Employee("Nuria Vidal", "EMP-003");
WaitingRoom room = new WaitingRoom();
room.arrive(marta);
room.arrive(diego);
room.arrivePriority(nuria); // jumps to the front
System.out.println("Next: " + room.next().getName()); // Nuria Vidal
System.out.println("Position of Marta: " + room.positionOf(marta)); // 2
System.out.println("Diego leaves: " + room.leave(diego)); // true
room.closeDesk();Next: Nuria Vidal Position of Marta: 2 Diego leaves: true --- Closing the desk: 2 waiting --- Turn 1: Nuria Vidal (EMP-003) Turn 2: Marta Ruiz (EMP-001) --- Desk closed ---
The exercise illustrates a Deque's cost distribution well: everything that happens at the ends is O(1), and everything that requires looking inside is O(n). That fits the domain perfectly: arriving, being served and inspecting the next one are constant; leaving from the middle of the queue or asking for a position are exceptional.
Notice arrivePriority too: it is O(1) thanks to the circular array. An ArrayList would have to shift every element.
Solution 2
package com.nexussoftware.bibliotech.service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
/** A catalogue maintenance task. Priority 1 = maximum urgency. */
record MaintenanceTask(String description, int priority, int creationDay) {
MaintenanceTask {
if (description == null || description.isBlank()) { description = "(no description)"; }
if (priority < 1 || priority > 10) { priority = 5; }
if (creationDay < 0) { creationDay = 0; }
}
}
public class TaskScheduler {
private final Queue<MaintenanceTask> queue;
public TaskScheduler() {
// Lowest priority number first. On a tie, the oldest.
// The tie-breaker makes the order DETERMINISTIC and also fair: between two
// equal urgencies, the one that has waited longest wins.
this.queue = new PriorityQueue<>(
Comparator.comparingInt(MaintenanceTask::priority)
.thenComparingInt(MaintenanceTask::creationDay));
}
/** O(log n): the element floats up through the heap. */
public void schedule(MaintenanceTask t) {
if (t != null) { queue.offer(t); }
}
/** O(1): the root of the heap. */
public MaintenanceTask next() {
return queue.peek();
}
/** O(log n): takes the root out and reorganises. */
public MaintenanceTask execute() {
return queue.poll();
}
/**
* The next N WITHOUT emptying the original queue.
* PriorityQueue's copy constructor duplicates the heap.
*/
public List<MaintenanceTask> upcoming(int howMany) {
Queue<MaintenanceTask> copy = new PriorityQueue<>(queue);
List<MaintenanceTask> result = new ArrayList<>();
for (int i = 0; i < howMany && !copy.isEmpty(); i++) {
result.add(copy.poll());
}
return result;
}
public int executeAll() {
int n = 0;
MaintenanceTask t;
while ((t = queue.poll()) != null) {
System.out.printf(" [P%d] day %3d %s%n", t.priority(), t.creationDay(),
t.description());
n++;
}
return n;
}
/** Shows that only poll() respects the priority order. */
public void demoUnorderedIterator() {
System.out.println("=== The iterator does NOT traverse in priority order ===");
System.out.println("toString():");
System.out.println(" " + queue);
System.out.print("for-each: ");
for (MaintenanceTask t : queue) { System.out.print("P" + t.priority() + " "); }
System.out.println();
System.out.print("forEach: ");
queue.forEach(t -> System.out.print("P" + t.priority() + " "));
System.out.println();
System.out.print("poll: ");
Queue<MaintenanceTask> copy = new PriorityQueue<>(queue);
MaintenanceTask t;
while ((t = copy.poll()) != null) { System.out.print("P" + t.priority() + " "); }
System.out.println(" <- the ONLY guaranteed order");
System.out.println("Cause: the internal array only satisfies the heap property");
System.out.println("(each node <= its children), not a total order.");
}
public int pending() { return queue.size(); }
}A test:
TaskScheduler p = new TaskScheduler();
p.schedule(new MaintenanceTask("Check damaged copies", 5, 100));
p.schedule(new MaintenanceTask("Replace damaged DVD", 1, 130));
p.schedule(new MaintenanceTask("Annual inventory", 8, 90));
p.schedule(new MaintenanceTask("Fix incorrect ISBNs", 1, 110));
p.schedule(new MaintenanceTask("Clean shelves", 5, 95));
System.out.println("Next: " + p.next().description());
System.out.println("Next 2: ");
p.upcoming(2).forEach(t -> System.out.println(" " + t.description()));
System.out.println("Pending after querying: " + p.pending());
p.demoUnorderedIterator();
System.out.println("\n--- Executing all ---");
p.executeAll();Next: Fix incorrect ISBNs Next 2: Fix incorrect ISBNs Replace damaged DVD Pending after querying: 5 === The iterator does NOT traverse in priority order === toString(): [MaintenanceTask[...priority=1, creationDay=110], ...] for-each: P1 P1 P8 P5 P5 forEach: P1 P1 P8 P5 P5 poll: P1 P1 P5 P5 P8 <- the ONLY guaranteed order ... --- Executing all --- [P1] day 110 Fix incorrect ISBNs [P1] day 130 Replace damaged DVD [P5] day 95 Clean shelves [P5] day 100 Check damaged copies [P8] day 90 Annual inventory
Three points sum up the lesson. First, upcoming(2) does not empty the queue because it works on a copy of the heap; forgetting that is the most frequent mistake when writing a "top N". Second, the iterator produces P1 P1 P8 P5 P5 —unordered— while poll produces P1 P1 P5 P5 P8: the visual demonstration that the heap is not a total order. And third, the tie-breaker by creationDay means that between the two P1 tasks the one from day 110 comes out first, the older one: without it, the order would be arbitrary.
Solution 3
package com.nexussoftware.bibliotech.service;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class RecommenderComparison {
private final Map<String, List<String>> related;
public RecommenderComparison(Map<String, List<String>> related) {
this.related = related;
}
/**
* BREADTH (BFS): a FIFO queue. It explores by LEVELS: first all the
* direct neighbours, then the neighbours of those, and so on.
*/
public List<String> breadthFirst(String origin, int maxDepth) {
return traverse(origin, maxDepth, true);
}
/**
* DEPTH (DFS): a LIFO stack. It goes as far as it can down one branch
* before backtracking. SAME code, only the end it takes from changes.
*/
public List<String> depthFirst(String origin, int maxDepth) {
return traverse(origin, maxDepth, false);
}
private List<String> traverse(String origin, int maxDepth, boolean breadth) {
List<String> result = new ArrayList<>();
Set<String> visited = new HashSet<>();
Deque<String> toVisit = new ArrayDeque<>();
Deque<Integer> depths = new ArrayDeque<>();
toVisit.offerLast(origin);
depths.offerLast(0);
visited.add(origin);
while (!toVisit.isEmpty()) {
// THE ONLY DIFFERENCE between BFS and DFS is on this line:
// pollFirst -> FIFO -> breadth
// pollLast -> LIFO -> depth
String current = breadth ? toVisit.pollFirst() : toVisit.pollLast();
int depth = breadth ? depths.pollFirst() : depths.pollLast();
if (depth > 0) { result.add(current); }
if (depth >= maxDepth) { continue; }
for (String neighbour : related.getOrDefault(current, List.of())) {
if (visited.add(neighbour)) { // add returns false if it was there (05-06)
toVisit.offerLast(neighbour);
depths.offerLast(depth + 1);
}
}
}
return result;
}
public static void main(String[] args) {
Map<String, List<String>> graph = Map.of(
"978-0000000001", List.of("978-0000000002", "DVD-0007"),
"978-0000000002", List.of("978-0000000003"),
"978-0000000003", List.of("REV-2024-03"),
"DVD-0007", List.of("REV-2024-03")
);
RecommenderComparison r = new RecommenderComparison(graph);
System.out.println("BREADTH (BFS): " + r.breadthFirst("978-0000000001", 3));
System.out.println("DEPTH (DFS): " + r.depthFirst("978-0000000001", 3));
}
}BREADTH (BFS): [978-0000000002, DVD-0007, 978-0000000003, REV-2024-03] DEPTH (DFS): [DVD-0007, REV-2024-03, 978-0000000002, 978-0000000003]
Why they differ. The only different line is where the next node is taken from. With pollFirst (FIFO), the node that has been waiting longest comes out first, so all those at distance 1 are exhausted before touching those at distance 2: the result comes out sorted by closeness. With pollLast (LIFO), the last one added comes out, which is always the deepest, so the algorithm goes down to the bottom of a branch before coming back.
When each one is preferable:
| I need | Traversal |
|---|---|
| The shortest path in number of hops | Breadth |
| Recommendations sorted by closeness | Breadth |
| To explore a whole subtree before moving to the next | Depth |
| To detect cycles, order dependencies, solve mazes | Depth |
| The graph is very wide (few levels, many neighbours) | Depth (it uses less memory) |
| The graph is very deep (many levels) | Breadth (it avoids enormous stacks) |
That the same data structure, a Deque, produces two fundamentally different algorithms depending on which end it is consumed from is one of the most elegant ideas in programming, and it explains why Deque is the most versatile interface in the Collections Framework.
Conclusion
You have added to your repertoire the family of collections that answers "whose turn is it?". You know that a FIFO queue serves whoever has been waiting longest first and that its restriction —there is no arbitrary access— is precisely what guarantees the policy is respected.
You have mastered the Queue interface with its two families of methods and, above all, the criterion for choosing between them: add/remove/element throw an exception because they treat failure as an anomaly; offer/poll/peek return a special value because they treat an empty queue as a normal situation. And you have the standard idiom for consuming a queue: while ((x = queue.poll()) != null).
You understand ArrayDeque from the inside: a circular array with two indices that move instead of moving the elements, with a power-of-two capacity, growth by doubling and amortised O(1) cost at both ends. You know why it rejects nulls —because poll and peek use them as an "empty" signal— and why it is the default option for queues and stacks: less memory, better cache locality and no pressure on the collector compared with LinkedList.
You know Deque as the most versatile interface in the Framework: twelve methods across both ends, plus the queue aliases (offer/poll/peek) and the stack ones (push/pop), which let it be a FIFO queue, a LIFO stack or a genuine double-ended queue with the same class. And you know that it is not a List: there is no get(i).
You handle PriorityQueue and its binary heap: the root is always the minimum, offer and poll cost O(log n) because the element floats or sinks through a tree of logarithmic height, and peek is O(1). You know it needs Comparable or a Comparator, that it allows no null, that it is not stable, and —the detail that catches everybody out— that its iterator, its toString and its forEach do not traverse in priority order, because the internal array only satisfies the heap property. The only correct way is to empty it with poll, or do it on a copy if you need to keep it.
You know that the BlockingQueues exist with their take and put operations that wait, and that the producer-consumer pattern is built on them, with its real implementation arriving in module 8; and that ordinary queues must never be shared between threads without synchronisation. And you have seen their three canonical uses: buffers that absorb differences in pace, task scheduling and breadth-first search, where you discovered something remarkable: swapping pollFirst for pollLast on a single line turns a BFS into a DFS.
BiblioTech now has its ReservationQueue rewritten on top of ArrayDeque —with urgent reservations that jump to the front in O(1) thanks to the circular array— and a NoticeQueue with a PriorityQueue that serves first the loan with the most days overdue, applying each material type's own term without the processing code knowing anything about that logic.
In the next lesson, Stack, you explore the other access policy: LIFO, the last one in is the first one out. You will see what it is really for —undo, evaluating expressions, depth-first traversal and the JVM's own call stack, with its connection to StackOverflowError and the recursion of 03-03—, why the legacy Stack class is discouraged (it extends Vector, it is synchronised and its iterator walks the opposite way to what you would expect, demonstration included), how to use Deque as a stack and why it is the official recommendation, how to implement your own stack with an array to understand the structure from the inside, and two complete practical cases: balanced brackets and a browsing history with undo and redo using two stacks. In BiblioTech an operation stack will appear that allows the last catalogue registration or removal to be undone.
Java Programming Course
Module 1: Introduction to Java
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
- Console Input and Output
- Your First Complete Program: BiblioTech
Module 2: Control Flow
- Conditional Statements
- Loops
- Switch Statements
- Break and Continue
- Debugging and Execution Traces
- Project: The BiblioTech Interactive Menu
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- The Object Class: equals, hashCode and toString
Module 4: Advanced Object-Oriented Programming
- Interfaces
- Abstract Classes
- Inner Classes
- Anonymous Classes
- Lambda Expressions
- Functional Interfaces and Method References
- Enums and Records
Module 5: Data Structures and Collections
- Arrays
- The Collections Framework
- ArrayList
- LinkedList
- HashMap
- HashSet
- Queue and Deque
- Stack
- Sorting and Searching Collections
Module 6: Exception Handling
- Introduction to Exceptions
- The Try-Catch Block
- Throw and Throws
- Custom Exceptions
- The Finally Block
- Try-with-resources and AutoCloseable
- Error Handling Strategies and Logging
Module 7: File Input/Output
- Reading Files
- Writing Files
- File Streams
- BufferedReader and BufferedWriter
- Serialization
- The NIO.2 API: Path and Files
- Interchange Formats: CSV and Properties
Module 8: Multithreading and Concurrency
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
- Concurrent Collections and Atomic Variables
- Asynchronous Tasks with CompletableFuture
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection
- The Modern HTTP Client
Module 10: Advanced Topics
- Generics
- Annotations
- Reflection
- Java 8 Features: Streams and Optional
- Dates and Times with java.time
- Java 9 and Beyond
- Memory, Garbage Collection and Performance
Module 11: Java Frameworks and Libraries
- Introduction to Java Frameworks
- Spring Framework
- Hibernate
- JUnit
- Maven
- Advanced Testing with Mockito
- Essential Ecosystem Libraries
