10.7
Queue, Deque, and PriorityQueue
Who is served next? A print queue answers "whoever arrived first". A hospital answers "whoever is most urgent". Both are queues, and the second one is a tree hiding inside an array.
Previously on
Section 10.4 gave you List, which is about position. Section 10.5 gave you Set and Map, which are about uniqueness and lookup.
None of them answers the question a lot of real systems ask: who is next?
The problem
A printer has jobs waiting. A web server has requests waiting. A hospital has patients waiting.
All three need to know who to serve next, and they do not agree on the answer.
The printer serves in arrival order. First in, first out. Fair, and simple.
The hospital serves the most urgent patient, whatever time they arrived. Somebody who walks in bleeding goes ahead of somebody who has been waiting an hour with a sore throat.
Both are queues. They differ only in what “next” means, and Java gives you an interface for the shape and different implementations for the two answers.
The idea
Queue: first in, first out
Queue<String> jobs = new ArrayDeque<>();
jobs.offer("first");
jobs.offer("second");
jobs.offer("third");
jobs.peek(); // first look, do not remove
jobs.poll(); // first take it off
// jobs is now [second, third]You add at the back and take from the front. That is the whole idea.
The two families of methods
Every queue operation comes in two versions, and the difference is what happens when things go wrong.
| Returns a value | Throws an exception | |
|---|---|---|
| Add | offer(e) returns false if full | add(e) throws |
| Take | poll() returns null if empty | remove() throws NoSuchElementException |
| Look | peek() returns null if empty | element() throws NoSuchElementException |
Use the returning versions when empty is normal, which it usually is. A worker loop checking for jobs expects to find none most of the time.
Use the throwing versions when empty means a bug. You want to find out at that line, not ten lines later when the null finally causes a NullPointerException.
Deque: a queue with two ends
Deque is short for double ended queue, and it is pronounced “deck”. You can add and remove at both ends.
Deque<Integer> d = new ArrayDeque<>();
d.addFirst(2);
d.addLast(3);
d.addFirst(1); // [1, 2, 3]
d.pollFirst(); // 1
d.pollLast(); // 3Which means one class can be either a queue or a stack:
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.pop(); // 2. Last in, first out.Under the hood
Going deeperPriorityQueue: the hospital
PriorityQueue<Integer> pq = new PriorityQueue<>(List.of(50, 10, 40, 20, 30));
System.out.println(pq); // [10, 20, 40, 50, 30] <- not sorted
while (!pq.isEmpty()) System.out.print(pq.poll() + " ");
// 10 20 30 40 50 <- sorted on the way outPrint it and the contents look wrong. Poll it and everything arrives in order.
That is not a bug. A PriorityQueue never sorts itself. It promises one thing only: the element at the front is the smallest.
A heap
Underneath is a heap: a tree with one rule.
Every parent is smaller than both of its children.
10
/ \
20 40
/ \
50 30That rule pins the smallest value to the root. It says nothing about the order of the rest, which is why [10, 20, 40, 50, 30] is a perfectly valid heap.
Keeping it fully sorted would cost far more work, and would be wasted. You only ever look at the front.
The tree lives in a flat array
Here is the clever part, and it is the reason a heap is fast.
There are no Node objects and no references. The tree is stored row by row in an ordinary array, and every relationship is arithmetic:
| For the node at index i | Formula | |
|---|---|---|
| Left child | 2i + 1 | |
| Right child | 2i + 2 | |
| Parent | (i - 1) / 2 |
Check it on [10, 20, 30, 40, 50, 60, 70]:
index: 0 1 2 3 4 5 6
value: 10 20 30 40 50 60 70
node 0 (10) -> children at 1 and 2, holding 20 and 30
node 1 (20) -> children at 3 and 4, holding 40 and 50
node 2 (30) -> children at 5 and 6, holding 60 and 70No node object is needed for each position. The index relationships from Section 4.2 let the implementation move between parent and child positions directly. A common JVM stores the backing array compactly, which can improve cache locality, but Java code does not receive or calculate physical addresses.
Adding and removing
offer(e): sift up
- Put it at the end of the arrayThe next free slot, which is the bottom of the tree.
- Compare it with its parent at (i - 1) / 2If it is smaller, swap them.
- Repeat until it is bigger than its parentOr until it reaches the root.
poll(): sift down
- Take the root. That is your answer.It is the smallest, by the heap rule.
- Move the last element into the rootTo keep the array packed with no gaps.
- Compare with its smaller child, and swap if neededThen repeat down the tree until it is smaller than both children.
Both walk one path from top to bottom, so both cost about log n steps. On a million elements that is around 20 comparisons, not a million.
Choosing your own order
By default a PriorityQueue puts the smallest first, using the natural order of the elements. To change that, hand it a Comparator:
// largest first
PriorityQueue<Integer> maxFirst = new PriorityQueue<>(Comparator.reverseOrder());
// most urgent patient first
PriorityQueue<Patient> ward =
new PriorityQueue<>(Comparator.comparingInt(Patient::getUrgency).reversed());Which raises the question that has now been deferred three times: how does Java know which of two objects comes first?
For Integer and String it already knows. For your own class it does not, and if you do not tell it, PriorityQueue throws ClassCastException the moment you add a second element.
That is Section 10.8, next.
The implementations
| Class | What it is, and when to use it | |
|---|---|---|
| ArrayDeque | a circular array | the default. Use it as a queue or a stack. |
| LinkedList | also implements Deque | works, and ArrayDeque is faster |
| PriorityQueue | a heap in an array | when order of service is not arrival order |
| Stack | extends Vector | never. Use ArrayDeque. |
ArrayDeque is circular: when the end of the array is reached it wraps around to the front, so adding at either end stays cheap and nothing has to shift.
What it costs
A queue gives up positions entirely. No get(i), no indexOf. If you need to look into the middle, a queue is the wrong shape.
PriorityQueue gives up order everywhere except the front. Printing one, iterating one, or converting one to a list gives you heap order, and that has surprised a lot of people into shipping a bug.
It is also unstable. Two elements of equal priority can come out in either order, and the order can change between runs. If arrival order matters as a tiebreaker, you have to build that into the comparison yourself.
Four method names for two operations is a lot to remember, and choosing the throwing one by accident turns a normal empty queue into an exception.
What you get is exactly the right amount of structure. A heap does not waste effort sorting things you will never look at, which is why finding the most urgent of a million items costs about twenty comparisons.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
`Queue` has `poll()` and `remove()`, and `peek()` and `element()`. Each pair does the same thing. Why does Java have both?
Show the answer
They differ in what happens when the queue is empty.
poll()andpeek()returnnull.remove()andelement()throwNoSuchElementException.Use the returning pair when empty is normal, which it usually is: a worker loop that checks for jobs expects to find none sometimes.
Use the throwing pair when empty means something has gone wrong, and you want to find out immediately rather than get a null that travels for ten more lines before failing.
The same split exists for adding:
offer()returns false when the queue is full,add()throws.Print a `PriorityQueue` and you get `[10, 20, 40, 50, 30]`. That is not sorted. Is it broken?
Show the answer
No. A
PriorityQueuenever sorts itself. It only promises that the front element is the smallest.Underneath is a heap: a tree where every parent is smaller than its children. That rule fixes the root and says nothing about the order of anything else.
[10, 20, 40, 50, 30]satisfies it.Keeping it fully sorted would cost far more, and would be wasted, because you only ever look at the front.
The trap: iterating a PriorityQueue gives heap order, not sorted order. To get sorted order you must
poll()until it is empty.How does a tree fit inside a flat array with no references between nodes?
Show the answer
By arithmetic. Store the tree row by row, and every relationship becomes a calculation.
For the node at index
i: its children are at2i + 1and2i + 2, and its parent is at(i - 1) / 2.Check it on
[10, 20, 30, 40, 50, 60, 70]. Node 0 holds 10, and its children are at 1 and 2, holding 20 and 30. Node 1's children are at 3 and 4, holding 40 and 50.No
Nodeobjects, no references, no memory scattered around the heap. It is the same index arithmetic as Section 4.2, and it is why a heap is so much faster than a tree built from objects.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises95 pointsabout 110 minutes
Four Names, Two Operations
ex-10-7-aFour method names for two operations, and the difference only shows up when the queue is empty.
Predict which two throw, then check.
Then use both properly. A worker loop that polls for jobs should treat empty as normal, because it usually is. A method that has already checked the queue is not empty should use the throwing version, so a broken assumption fails immediately instead of producing a null that travels.
That is the whole distinction, and choosing by habit rather than by meaning is how nulls end up somewhere confusing.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class TwoFamilies {
static String tryOnEmpty(String method)
static List<String> peekThenPoll()
static List<String> drainQuietly(Queue<String> q)
static String mustHaveOne(Queue<String> q)
}tryOnEmpty takes "poll", "peek", "remove" or "element", tries it on an empty queue, and returns "null" or "NoSuchElementException". peekThenPoll returns four strings: what peek gave, the size after it, what poll gave, and the size after that.
What your program must do
- Predict which methods throw on an empty queue, then verify
- Show the difference between peek and poll, including what happens to the size
- Write a worker loop that treats empty as normal
- Write a check where empty is a bug, using the throwing version
import java.util.*;
public class TwoFamilies {
// "poll", "peek", "remove" or "element" on an EMPTY queue.
// Return "null" or "NoSuchElementException". Predict all four first.
static String tryOnEmpty(String method) {
return "null"; // TODO
}
// Offer job1 and job2. Return: what peek gave, the size after,
// what poll gave, the size after that. Four strings.
static List<String> peekThenPoll() {
return List.of(); // TODO
}
// Empty is NORMAL here. Take everything, stop when it runs out.
static List<String> drainQuietly(Queue<String> q) {
return List.of(); // TODO
}
// Empty means a BUG here. Pick the method that says so.
static String mustHaveOne(Queue<String> q) {
return null; // TODO
}
public static void main(String[] args) {
// TODO: print all four empty-queue results
// TODO: show peek does not remove and poll does
}
}
Hint 1
offer and add, poll and remove, peek and element. The first of each pair answers quietly, the second throws.Hint 2
while ((job = q.poll()) != null) is the quiet loop. It stops on its own when the queue empties.Hint 3almost the answer
A PriorityQueue Is Not Sorted
ex-10-7-bPrint a PriorityQueue and the contents look wrong. They are not.
Verify the heap rule yourself on the printed array, using the index arithmetic. Every parent is smaller than both its children, and that rule is satisfied by an order that looks scrambled.
Then poll it and watch sorted order appear on the way out.
Finish with the practical problem: getting a sorted view without emptying the queue. There is more than one way, and both involve copying, which tells you something about what the structure is willing to promise.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class NotSorted {
static PriorityQueue<Integer> queue()
static List<Integer> walkOrder(PriorityQueue<Integer> pq)
static List<Integer> pollOrder(PriorityQueue<Integer> pq)
static boolean heapRuleHolds(List<Integer> internal)
static List<Integer> sortedWithoutEmptying(PriorityQueue<Integer> pq)
}queue() holds 50, 10, 40, 20, 30. heapRuleHolds checks by index: for every position i, the values at 2i+1 and 2i+2 must not be smaller than the value at i.
What your program must do
- Show that walking the queue gives heap order, not sorted order
- Show that polling gives sorted order
- Verify the heap rule on the internal order, by index
- Produce a sorted list without destroying the queue
import java.util.*;
public class NotSorted {
static PriorityQueue<Integer> queue() {
return new PriorityQueue<>(List.of(50, 10, 40, 20, 30));
}
// The order a for-each loop gives you. This is NOT sorted order.
static List<Integer> walkOrder(PriorityQueue<Integer> pq) {
return List.of(); // TODO
}
// Poll everything out. THIS is sorted order.
static List<Integer> pollOrder(PriorityQueue<Integer> pq) {
return List.of(); // TODO
}
// Check by index: for every i, the values at 2i+1 and 2i+2 must not be smaller.
static boolean heapRuleHolds(List<Integer> internal) {
return false; // TODO
}
// A sorted list, with the queue still holding everything afterwards.
static List<Integer> sortedWithoutEmptying(PriorityQueue<Integer> pq) {
return List.of(); // TODO
}
public static void main(String[] args) {
// TODO: print the walk order and the poll order and compare them
// TODO: check the heap rule on the walk order
}
}
Hint 1
new ArrayList<>(pq) walks the queue with its iterator, which reads the array straight through.Hint 2
Hint 3almost the answer
The Heap Lives in an Array
ex-10-7-cBuild the structure inside PriorityQueue.
Two methods, about ten lines each, and no references anywhere. The tree exists only in the arithmetic that turns an index into its children.
Printing the array after each offer is the part worth doing. You can watch a small value climb one level at a time until it reaches its place, and nothing about the array looks like a tree while it happens.
Finish by counting comparisons on a large input. If your count is close to n times log n, your sift operations are walking one path rather than searching.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class MiniHeap {
public void offer(int v)
public int poll()
public int peek()
public int size()
public long comparisons()
public int[] internal()
}internal() returns a copy of the used part of the array, so the tests can check the heap rule after every offer. comparisons() counts every parent-child comparison you make. peek and poll on an empty heap throw NoSuchElementException.
What your program must do
- Implement offer with sift up and poll with sift down
- Offer twenty random numbers and confirm polling gives sorted output
- Print the internal array as it changes and watch small values rise
- Count comparisons for a large input and compare with what n log n predicts
import java.util.*;
public class MiniHeap {
private int[] a = new int[16];
private int size;
private long comparisons;
private static int parent(int i) { return (i - 1) / 2; }
private static int left(int i) { return 2 * i + 1; }
private static int right(int i) { return 2 * i + 2; }
// Put it at the end, then sift up while it is smaller than its parent.
// Grow the array first if it is full. Count every comparison.
public void offer(int v) {
// TODO
}
// Take the root, move the last value into its place, then sift down.
public int poll() {
return 0; // TODO
}
public int peek() {
return 0; // TODO
}
public int size() {
return size;
}
public long comparisons() {
return comparisons;
}
// A copy of the used part, so you can watch the shape change.
public int[] internal() {
return Arrays.copyOf(a, size);
}
public static void main(String[] args) {
// TODO: offer 20 random numbers, printing internal() after each one
// TODO: poll them all and confirm the output is sorted
// TODO: count comparisons for 1_000_000 offers and compare with n log n
}
}
Hint 1
Hint 2
Hint 3almost the answer
n log n, not n squared. Sifting walks the height of the tree, and a tree of a million values is only about twenty levels deep.The Registry Triage Queue
ex-10-7-dTwo queues over the same requests, giving different answers, and both correct for their job.
Start with arrival order. Then switch to urgency and watch the order change completely.
Run it once without telling the queue how to compare, and note that the exception arrives on the second element rather than the first. That timing is a clue about what a heap does on insert.
Then handle the tie. Atlas and Cipher have the same urgency, and the one who arrived first should go first. Getting the chained comparator in the right order is the exercise, and it leads directly into the next section.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Triage {
static List<Request> incoming()
static List<String> inArrivalOrder(List<Request> requests)
static List<String> byUrgency(List<Request> requests)
static String withoutTellingItHowToCompare(List<Request> requests)
static List<String> byUrgencyThenArrival(List<Request> requests)
}incoming() returns Atlas 3/1, Beacon 9/2, Cipher 3/3, Drift 7/4. Every serving method returns unit names in the order they were served. withoutTellingItHowToCompare returns "ok" or the name of the exception.
What your program must do
- Serve the requests in arrival order with a plain queue
- Serve them by urgency, highest first
- Show the exception you get when the queue is not told how to compare
- Break ties by arrival order and prove Atlas comes before Cipher
import java.util.*;
public class Triage {
record Request(String unit, int urgency, int arrivalOrder) { }
static List<Request> incoming() {
return List.of(
new Request("Atlas", 3, 1),
new Request("Beacon", 9, 2),
new Request("Cipher", 3, 3),
new Request("Drift", 7, 4));
}
// A plain FIFO queue. Return the unit names in the order served.
static List<String> inArrivalOrder(List<Request> requests) {
return List.of(); // TODO
}
// Highest urgency served first.
static List<String> byUrgency(List<Request> requests) {
return List.of(); // TODO
}
// A PriorityQueue with nothing to compare by. Return "ok" or the exception name.
static String withoutTellingItHowToCompare(List<Request> requests) {
return "ok"; // TODO
}
// Highest urgency first, and ties broken by who arrived first.
static List<String> byUrgencyThenArrival(List<Request> requests) {
return List.of(); // TODO
}
public static void main(String[] args) {
// TODO: print all four and compare them
}
}
Hint 1
Comparator.comparingInt(Request::urgency).reversed().Hint 2
Request is not Comparable, so a PriorityQueue built with no comparator has no way to order anything. It throws when you add, not when you build.Hint 3almost the answer
.thenComparingInt(Request::arrivalOrder) chains on a second question, asked only when the first one ties. Without it, two requests of the same urgency come out in whatever order the heap happened to leave them.After the credits
Three sections have now deferred the same question, and the next one answers it.
PriorityQueue needs to know which element is most urgent. TreeMap needs to know which key comes first. Collections.sort needs to know which element goes before which.
None of them can work it out. For Integer and String, Java already knows. For your Unit, your Patient, your Student, it has no idea, and the failure is not a compile error. It is a ClassCastException the moment you add a second element.
Section 10.8 is Comparable and Comparator. It has a contract, exactly like equals and hashCode in Section 8.3, and it fails just as quietly. Break it and sort can throw an exception, or worse, produce an order that is silently wrong.
It is the last section of Phase X, and it closes the last thread this phase left open.
Threads you opened in this section
- Queue and DequeBlockingQueue is how threads hand work to each other safely.Phase XIV. Concurrency
- HeapThe heap rule needs a comparison, and that is what Comparable supplies.10.8 - Comparable, Comparator, and Sorting
Queue and Deque will return in 10.8 - Comparable, Comparator, and Sorting