Queue model
The queue abstract data type: first-in first-out order, enqueue and dequeue, the front and the rear, what underflow and overflow mean, and the invariants every implementation must keep.
Learning goals
- State the FIFO rule and the queue operations
- Choose between the exception-throwing and value-returning methods of Java's Queue
- Explain underflow and overflow for bounded and unbounded queues
- List the invariants that every queue implementation must preserve
Prerequisites
A queue is the line at a ticket counter: whoever arrived first is served first, newcomers join at the back, and nobody may jump ahead.
FIFO
A queue is a collection with two working ends. New values join at the **rear**; values are served from the **front**. The value that has waited longest always leaves first: first in, first out (FIFO). This is the mirror image of a stack, where the newest value leaves first.
FIFO matches problems where fairness or arrival order matters: print jobs, network packets, tasks waiting for a worker, and the frontier of a breadth-first search. Run the animation and compare the output order with the order of the enqueues.
| Operation sequence | Queue after (front to rear) | Returned |
|---|---|---|
| enqueue 1, enqueue 2, enqueue 3 | 1, 2, 3 | - |
| dequeue | 2, 3 | 1 |
| enqueue 4 | 2, 3, 4 | - |
| dequeue, dequeue | 4 | 2, then 3 |
Queue: enqueue, dequeue, peek, overflow and underflow
Interactive · edit the inputqueue · front → rear · size 0 of 3
An empty queue. Values join at the rear and leave from the front, so the oldest value is always served first (FIFO).
1class BoundedQueue {2private final ArrayDeque<Integer> q = new ArrayDeque<>(); private final int capacity;3boolean offer(int x) {4if (q.size() == capacity) return false; // overflow: reject5q.addLast(x); return true; // join at the rear6}7Integer poll() {8return q.pollFirst(); // front leaves first; null when empty9}10Integer peek() { return q.peekFirst(); } // front, not removed11}
Live state
size
0
capacity
3
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | size=0 capacity=3 | An empty queue. Values join at the rear and leave from the front, so the oldest value is always served first (FIFO). |
| 2 | L5 | op=offer(5) size=1 rear=5 | addLast(5): 5 joins behind everything already waiting. Nothing else moves. |
| 3 | L5 | op=offer(8) size=2 rear=8 | addLast(8): 8 joins behind everything already waiting. Nothing else moves. |
| 4 | L5 | op=offer(3) size=3 rear=3 | addLast(3): 3 joins behind everything already waiting. Nothing else moves. |
| 5 | L10 | op=peek() front=5 size=3 | peek() returns 5 without removing it. |
| 6 | L8 | op=poll() returned=5 size=2 | pollFirst() returns 5, the value that has waited longest. The new front is 8. |
| 7 | L5 | op=offer(7) size=3 rear=7 | addLast(7): 7 joins behind everything already waiting. Nothing else moves. |
| 8 | L8 | op=poll() returned=8 size=2 | pollFirst() returns 8, the value that has waited longest. The new front is 3. |
| 9 | L8 | op=poll() returned=3 size=1 | pollFirst() returns 3, the value that has waited longest. The new front is 7. |
| 10 | L8 | op=poll() returned=7 size=0 | pollFirst() returns 7, the value that has waited longest. The queue is empty again. |
| 11 | L11 | size=0 failed=0 return=[] | Final queue (front to rear): []; 0 operations failed. |
Enqueue
Enqueue adds a value at the rear. It touches only the rear end, so a good implementation does it in O(1) and never moves the values already waiting. In Java the Queue interface offers two methods: offer(x) returns false if the queue cannot accept the value, and add(x) throws IllegalStateException in that case.
For an unbounded queue such as ArrayDeque both methods always succeed, so the difference only matters for bounded queues such as ArrayBlockingQueue.
1static void enqueueAll(Queue<Integer> queue, int[] values) {2for (int v : values) {3if (!queue.offer(v)) { // bounded queue: reject without throwing4System.out.println("rejected " + v);5}6}7}
Dequeue
Dequeue removes and returns the front value. Java again gives a pair of methods: poll() returns null when the queue is empty, remove() throws NoSuchElementException. Because ArrayDeque does not allow null elements, a null from poll() can only mean empty.
Prefer poll() in loops that drain a queue and remove() when an empty queue would be a bug you want to see immediately.
| Purpose | Throws an exception | Returns a special value |
|---|---|---|
| Insert at the rear | add(x) | offer(x): false when full |
| Remove the front | remove() | poll(): null when empty |
| Examine the front | element() | peek(): null when empty |
Front and rear
The **front** (also called the head) is the value that will leave next; the **rear** (the tail) is the value that joined last. peek() reads the front without removing it. The Queue interface has no method to read the rear: use a Deque (peekLast()) or keep your own reference when you need it.
An implementation therefore tracks two positions. An array queue keeps two indices, a linked queue keeps two node references. The next lessons show both.
- front = oldest value = next to be dequeued
- rear = newest value = last one enqueued
- With one value, front and rear are the same element
- peek() looks at the front only; it never changes the queue
Underflow/overflow
**Underflow** is dequeuing or peeking at an empty queue. **Overflow** is enqueuing into a queue that is full. Only bounded queues can overflow; an ArrayDeque or LinkedList simply grows.
ArrayBlockingQueue is a bounded queue: offer returns false when it is full and add throws IllegalStateException with the message Queue full. Choose the Bounded option in the animation, give it a capacity of 2 and enqueue three values to watch the overflow; the “Underflow” preset shows poll and peek on an empty queue.
Queue: enqueue, dequeue, peek, overflow and underflow
Interactive · edit the inputqueue · front → rear · size 0 of 3
An empty queue. Values join at the rear and leave from the front, so the oldest value is always served first (FIFO).
1class BoundedQueue {2private final ArrayDeque<Integer> q = new ArrayDeque<>(); private final int capacity;3boolean offer(int x) {4if (q.size() == capacity) return false; // overflow: reject5q.addLast(x); return true; // join at the rear6}7Integer poll() {8return q.pollFirst(); // front leaves first; null when empty9}10Integer peek() { return q.peekFirst(); } // front, not removed11}
Live state
size
0
capacity
3
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | size=0 capacity=3 | An empty queue. Values join at the rear and leave from the front, so the oldest value is always served first (FIFO). |
| 2 | L5 | op=offer(5) size=1 rear=5 | addLast(5): 5 joins behind everything already waiting. Nothing else moves. |
| 3 | L5 | op=offer(8) size=2 rear=8 | addLast(8): 8 joins behind everything already waiting. Nothing else moves. |
| 4 | L5 | op=offer(3) size=3 rear=3 | addLast(3): 3 joins behind everything already waiting. Nothing else moves. |
| 5 | L10 | op=peek() front=5 size=3 | peek() returns 5 without removing it. |
| 6 | L8 | op=poll() returned=5 size=2 | pollFirst() returns 5, the value that has waited longest. The new front is 8. |
| 7 | L5 | op=offer(7) size=3 rear=7 | addLast(7): 7 joins behind everything already waiting. Nothing else moves. |
| 8 | L8 | op=poll() returned=8 size=2 | pollFirst() returns 8, the value that has waited longest. The new front is 3. |
| 9 | L8 | op=poll() returned=3 size=1 | pollFirst() returns 3, the value that has waited longest. The new front is 7. |
| 10 | L8 | op=poll() returned=7 size=0 | pollFirst() returns 7, the value that has waited longest. The queue is empty again. |
| 11 | L11 | size=0 failed=0 return=[] | Final queue (front to rear): []; 0 operations failed. |
Queue invariants
An invariant is a fact that must hold after every operation. If any of these breaks, the structure is corrupt, and checking them is the fastest way to debug a hand-written queue.
| Invariant | Why it matters |
|---|---|
| size = enqueues - successful dequeues | No value is lost or duplicated |
| Values leave in the order they arrived | This is the definition of FIFO |
| The queue is empty exactly when size == 0 | Underflow must be detectable |
| Bounded queue: size never exceeds capacity | Overflow must be detectable |
| Enqueue changes only the rear, dequeue only the front | Both are O(1) without shifting |
Practice ladder
Now try it yourself
Easy
- Show the queue after enqueue 1, 2, 3, dequeue, enqueue 4
- What does ArrayDeque.poll() return on an empty queue?
Medium
- Why can ArrayDeque not store null while LinkedList can?
- When would you prefer offer over add?
Advanced
- Design a bounded queue API where a full queue blocks the producer instead of rejecting
Key takeaways
- A queue is FIFO: enqueue at the rear, dequeue at the front, both O(1).
- Queue offers exception-throwing (add, remove, element) and value-returning (offer, poll, peek) methods.
- Underflow and overflow must be handled; only bounded queues can overflow.