Skip to content
Ameen Abozaid
PublishedBEG-040-L05

Queues

  • Lesson 5 of 7
  • 10 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

A stack serves the newest item first. A queue does the opposite: it serves the oldest item first. The picture is a line at a shop: new customers join at the back, the cashier serves the person at the front, and nobody jumps the line.

This order is called FIFO: first in, first out. It is the natural shape of fairness and of work that must be done in the order it arrived: print jobs, tasks waiting for a worker, and the messages that move between services.

By the end you will use Queue with offer, poll, and peek, understand why a naive array queue is slow and how a circular array or a linked list fixes it, and be able to choose between a stack and a queue for a given problem.

Why this matters: queues are everywhere in backends, from the request waiting for a thread to a message broker between systems. Later modules build on the idea you learn here.

This lesson builds on Lessons 1 to 4 and on the Queues and deques lesson of the Collections module (BEG-030).

Learning Objectives

By the end of this lesson you will be able to:

  • Define a queue and FIFO.
  • Use offer, poll, and peek on a Queue, and predict their behavior on an empty queue.
  • Explain why a queue stored in a plain array with shifting is slow (O(n) per poll).
  • Explain how a circular array and a linked list make both ends O(1).
  • Implement a queue with a tail reference.
  • Choose between a stack and a queue from the shape of a problem.

Written Explanation

The operations

OperationMeaningCost (good implementation)
offer(x)Join the back of the line (enqueue)O(1)
poll()Remove and return the front item, or null if empty (dequeue)O(1)
peek()Look at the front item without removing it, or null if emptyO(1)

Trace:

text
offer(A)        front -> A <- back
offer(B)        front -> A B <- back
offer(C)        front -> A B C <- back
poll() -> A     front -> B C <- back

The first in (A) was the first out. That is FIFO.

Java: Queue and ArrayDeque

Queue<E> is the interface for this behavior, and ArrayDeque is a common implementation. The JDK documentation for ArrayDeque says it is likely to be faster than LinkedList when used as a queue, and that it prohibits null items.

java
Queue<String> q = new ArrayDeque<>();
q.offer("Ana");
q.offer("Omar");
String next = q.poll();   // "Ana"

The Queue interface has two versions of some operations. offer, poll, and peek signal problems with a special value (false or null). The older-style add, remove, and element throw exceptions instead. For a general queue, prefer offer and poll, and remember that poll returns null when the queue is empty. Since ArrayDeque does not allow null items, a null from poll always means "empty".

Why an array queue is tricky

Suppose you store the queue in a plain array where the front is always index 0. offer writes at size (cheap). But poll removes index 0, and to keep the front at index 0 you must shift every other item left: O(n) each time. That is the problem from Lesson 1. A queue built on this idea is correct but slow, and the slowness grows with the queue's length.

Two standard fixes make both ends O(1):

  1. A linked list with head and tail. offer attaches at the tail; poll detaches the head. Nothing shifts.
  2. A circular array (ring buffer). Keep an index head for the front and let positions wrap around the end of the array using the remainder operator. poll just moves head forward by one; no shifting.
text
capacity 3:  [ _ , 2 , 3 ]    head = 1, size = 2
offer(4) goes to (head + size) % 3 = 0 -> [ 4 , 2 , 3 ]

The back of the queue wrapped around to the start of the array. ArrayDeque works on a related idea: an array with indexes that move at both ends, growing by copying when full, as in Lesson 2.

A linked queue

java
void offer(T v) {
    Node<T> n = new Node<>(v);
    if (tail == null) { head = n; tail = n; }
    else { tail.next = n; tail = n; }
    size++;
}

T poll() {
    if (head == null) return null;
    T v = head.value;
    head = head.next;
    if (head == null) tail = null;   // queue became empty
    size--;
    return v;
}

This is Lesson 3's addLast and removeFirst. The line if (head == null) tail = null is the same trap as before: when the last node leaves, the tail must not keep pointing at it.

Choosing: stack or queue?

Ask in which order items should come back out.

Newest firstOldest first
Stack (LIFO)Queue (FIFO)
Undo, back button, call stack, bracket matchingPrint jobs, task processing, customers waiting, messages

Queues also drive breadth-first search (you will meet this when graphs appear) and round-robin scheduling, where each task takes a turn and then rejoins the back.

Other kinds of queue

Real systems have variations that you will meet later: a priority queue serves the most important item first rather than the oldest, a bounded queue has a maximum length, and a deque (double-ended queue) lets you add and remove at both ends. ArrayDeque is a deque, which is why it works as both a stack and a queue.

Examples

Example 1: a short line (simple)

java
Queue<String> q = new ArrayDeque<>();
q.offer("Ana");
q.offer("Omar");
q.offer("Lina");
System.out.println(q.peek());
System.out.println(q.poll());
System.out.println(q.size() + " left, next is " + q.peek());
q.poll(); q.poll();
System.out.println(q.poll());

Real output:

text
Ana
Ana
2 left, next is Omar
null

peek looked at Ana without removing her; poll removed her. The last poll found an empty queue and returned null rather than throwing.

Example 2: processing print jobs (practical)

java
Queue<String> jobs = new ArrayDeque<>();
jobs.offer("report.pdf");
jobs.offer("photo.png");
jobs.offer("notes.txt");
while (!jobs.isEmpty()) {
    System.out.println("printing " + jobs.poll());
}

Output:

text
printing report.pdf
printing photo.png
printing notes.txt

Jobs are processed in arrival order. This while (!isEmpty()) poll() loop is the standard way to drain a queue.

Example 3: a ring buffer that wraps around (realistic)

A fixed-capacity queue, such as a buffer holding the last few incoming events, avoids shifting by wrapping:

java
static class RingQueue {
    private final int[] data;
    private int head, size;
    RingQueue(int cap) { data = new int[cap]; }

    boolean offer(int v) {
        if (size == data.length) return false;          // full
        data[(head + size) % data.length] = v;
        size++;
        return true;
    }

    Integer poll() {
        if (size == 0) return null;
        int v = data[head];
        head = (head + 1) % data.length;
        size--;
        return v;
    }
}

With capacity 3 the program offered 1, 2, 3, 4, polled twice, offered 5 and 6, and polled four times. Real output:

text
true true true false
1 2
true true
3 5 6 null

The fourth offer(4) was rejected because the buffer was full. After two polls there were two free slots, so 5 and 6 went into positions that wrapped around the end of the array. The values still came out in the order they went in: 3, 5, 6. No item was ever shifted.

Backend Insight

Queues are the backbone of asynchronous work in backends:

  • Work queues. A web request that triggers slow work, such as sending an email or generating a report, can put a job on a queue and return immediately; background workers take jobs from the front.
  • Message queues. Systems such as message brokers pass messages between services in order, which is the idea behind the module on messaging (MID-026 in the roadmap). They are queues at a larger scale, with extra concerns such as persistence and delivery guarantees that this module does not cover.
  • Backpressure. If items arrive faster than they leave, a queue grows. An unbounded queue then eats memory, so real systems limit its length or push back on the sender. That is why bounded queues exist.
  • Fairness. FIFO means no task waits forever behind later arrivals.

Remember the cost model you built: a queue is only O(1) at both ends if the structure underneath supports it. That is why ArrayDeque and linked implementations are chosen, and an ArrayList with remove(0) is not.

Common Mistakes

Mistake 1: Using a plain list and remove(0) as a queue

  • Incorrect: List<String> q = new ArrayList<>(); ... q.remove(0); Each dequeue shifts the rest: O(n).
  • Correct: Queue<String> q = new ArrayDeque<>(); ... q.poll();

Mistake 2: Forgetting the tail when the queue empties

In a linked queue, when the last node is polled, set tail = null too, or the next offer will attach to a node that is no longer in the queue.

Mistake 3: Treating null from poll as a value

poll returns null when the queue is empty. Check isEmpty() or null before using the result. And never offer(null) to an ArrayDeque; it throws NullPointerException.

Mistake 4: Mixing stack and queue method names

On a Deque, push adds at the front and offer adds at the back. Mixing them in one object gives confusing order. Use one style per object.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. offer("X"), offer("Y"), offer("Z"), poll(), poll(). What does peek() return?

Show solution A1

A1. After three offers the queue is X, Y, Z (front first). Two polls remove X and Y. peek() returns Z.

A2. What does poll() return on an empty Queue? What does remove() do?

Show solution A2

A2. poll() returns null. remove() throws NoSuchElementException.

A3. Choose stack or queue: (a) support tickets answered in the order received, (b) pages in a "go back" history, (c) a game's turn order where each player plays and rejoins the end.

Show solution A3

A3. (a) queue, (b) stack, (c) queue (the player who just played goes to the back).

Level B: Application

B1. A queue is stored in an array with the front at index 0 and holds 10,000 items. How many items shift on one poll? How many in a circular array?

Show solution B1

B1. Shifting array: 9,999 items move left on each poll. Circular array: no items move; only the head index changes.

B2. Write a loop that moves tasks A, B, C through a round-robin: poll one, print it, offer it back to the end, repeat 5 times. What is printed?

Show solution B2

B2.

java
Queue<String> tasks = new ArrayDeque<>();
tasks.offer("A"); tasks.offer("B"); tasks.offer("C");
for (int i = 0; i < 5; i++) {
    String t = tasks.poll();
    System.out.print(t + " ");
    tasks.offer(t);
}

It prints A B C A B (real run).

B3. In the ring buffer with capacity 4, head = 3 and size = 2. At which array index does the next offer write?

Show solution B3

B3. (head + size) % capacity = (3 + 2) % 4 = 1. The write goes to index 1, having wrapped around.

Level C: Challenge

C1. Write LinkedQueue<T> with offer, poll, peek, and isEmpty using nodes with head, tail, and size. Which line prevents the stale tail?

Show solution C1

C1.

java
static class LinkedQueue<T> {
    private Node<T> head, tail;
    private int size;

    void offer(T v) {
        Node<T> n = new Node<>(v);
        if (tail == null) { head = n; tail = n; }
        else { tail.next = n; tail = n; }
        size++;
    }

    T poll() {
        if (head == null) return null;
        T v = head.value;
        head = head.next;
        if (head == null) tail = null;   // queue became empty
        size--;
        return v;
    }

    T peek() { return head == null ? null : head.value; }
    boolean isEmpty() { return size == 0; }
}

The line if (head == null) tail = null; prevents the stale tail. Testing with a, b, c: two polls gave ab, peek showed c, polling again then once more returned null with isEmpty() true, and a later offer("z") then poll() returned z (real run).

C2. Tickets arrive at two per second and are completed at one per second. After 60 seconds, how many are waiting in an unbounded queue? What problem does that show, and what could a backend do about it?

Show solution C2

C2. Arrivals minus completions is one per second, so after 60 seconds about 60 tickets are waiting, and the queue keeps growing. In a real system an unbounded queue grows until memory or patience runs out. A backend can add more workers, limit the queue length and reject or delay new work (backpressure), or speed up processing.

Mini Challenge

Build a bank counter simulation. Use a Queue<String> of customer names. Write a loop for 8 steps in which, at every step, one customer is served (if any), and on steps 1, 3, and 5 two new customers arrive first (name them C1, C2, and so on).

  1. Print what happens each step: who was served and how many are waiting.
  2. After step 8, print who is still waiting.

Check your work: customers are served in the exact order they arrived, and at no step do you call poll on an empty queue without handling null.

Summary & Next Lesson

Key takeaways

  • A queue serves items in arrival order: FIFO; offer, poll, peek are O(1) in a good implementation.
  • In Java, use Queue<T> q = new ArrayDeque<>(); poll and peek return null when empty.
  • An array queue that shifts on every poll is O(n); a circular array or a linked list with a tail keeps both ends O(1).
  • When a linked queue becomes empty, reset the tail as well as the head.
  • Stack for "newest first", queue for "oldest first"; message queues are this idea at a larger scale.

Next lesson

Next is "Choosing: ArrayList vs LinkedList vs ArrayDeque". It collects every cost you have seen into one decision table, so you can pick the right JDK structure quickly.

Self-check

  1. What does FIFO mean?
  2. What do offer, poll, and peek do?
  3. Why is ArrayList.remove(0) a poor dequeue?
  4. How does a circular array avoid shifting?
  5. Stack or queue: tasks processed in arrival order?
Show answers
  1. First in, first out: the oldest item leaves first.
  2. offer adds at the back, poll removes and returns the front (or null), and peek looks at the front without removing it.
  3. Removing index 0 shifts all the other items, so it costs O(n).
  4. A head index moves forward and positions wrap around the end using the remainder operator, so no items move.
  5. A queue.

References

  • Oracle, Java SE 21 API, java.util.ArrayDeque. Supports: offer, poll, peek semantics (null on empty), null elements prohibited, and likely to be faster than LinkedList as a queue.
  • Oracle, Java SE 21 API, java.util.Deque. Supports: the deque interface providing both stack and queue operations.
  • All outputs came from running the code on JDK 21. The throwing/special-value distinction between add/remove and offer/poll is general knowledge of the Queue interface that was not fetched from its own page for this lesson. The message-queue remarks are conceptual and not verified against any product documentation.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.