Java Queue and Deque
A queue is a collection that orders elements first-in-first-out (FIFO), like a checkout line: the first item added is the first one removed. A deque (short for "double-ended queue", pronounced "deck") is a more powerful cousin that lets you add and remove elements from both ends, so it can act as a queue, a stack, or a sliding window buffer. Both are core parts of the java.util Collections Framework and show up constantly in real code: task schedulers, undo histories, breadth-first search, and buffering.
Overview / How It Works
In Java, Queue<E> and Deque<E> are interfaces, not concrete classes — you always instantiate an implementing class and program against the interface type. Queue extends Collection and adds FIFO-oriented methods (insert at the tail, remove/inspect the head). Deque extends Queue and adds explicit "First" and "Last" variants of every operation, so you can treat it as a queue (FIFO), a stack (LIFO), or both at once.
The two implementations you will use almost all the time are:
LinkedList— a doubly linked list. Every element is wrapped in a node holding references to the previous and next nodes. Insertion/removal at either end is O(1), but each node costs extra memory (two references plus object header) and traversal is cache-unfriendly because nodes are scattered across the heap.ArrayDeque— a resizable circular array (a ring buffer). Internally it keeps a backingObject[]array plusheadandtailindices that wrap around the array bounds. Adding to either end just writes into the next free slot and moves the index; when the array fills up, it is reallocated to double the size and the elements are copied in order. Because the data is contiguous in memory,ArrayDequeis faster and more memory-efficient thanLinkedListfor almost every use case, and the JDK documentation explicitly recommends it overLinkedListfor stack and queue behavior.
There is also PriorityQueue, which implements Queue but is NOT a FIFO structure — it orders elements by natural ordering or a Comparator using a binary heap. It’s worth knowing it exists, but this lesson focuses on the FIFO/deque behavior of LinkedList and ArrayDeque.
Syntax
Queue<String> queue = new LinkedList<>(); // or new ArrayDeque<>()
Deque<String> deque = new ArrayDeque<>(); // or new LinkedList<>()
queue.offer("item"); // insert at the tail (returns false instead of throwing if it fails)
queue.poll(); // remove & return the head, or null if empty
queue.peek(); // look at the head without removing it, or null if empty
Every Queue method comes in two flavors: one that throws an exception on failure, and one that returns a sentinel value (false or null). Prefer the sentinel-returning versions unless a failure is truly exceptional in your program.
| Operation | Throws exception | Returns special value |
|---|---|---|
| Insert | add(e) |
offer(e) |
| Remove head | remove() |
poll() |
| Inspect head | element() |
peek() |
Deque mirrors this table but for both ends explicitly:
| Operation | First (head) | Last (tail) |
|---|---|---|
| Insert | addFirst(e) / offerFirst(e) |
addLast(e) / offerLast(e) |
| Remove | removeFirst() / pollFirst() |
removeLast() / pollLast() |
| Inspect | getFirst() / peekFirst() |
getLast() / peekLast() |
A Deque can also act as a stack using push(e) (alias for addFirst), pop() (alias for removeFirst), and peek() (alias for peekFirst).
Examples
Example 1: A Simple FIFO Task Queue
import java.util.LinkedList;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
Queue<String> printQueue = new LinkedList<>();
printQueue.offer("Report.pdf");
printQueue.offer("Invoice.docx");
printQueue.offer("Photo.png");
System.out.println("Queue: " + printQueue);
System.out.println("Next to print (peek): " + printQueue.peek());
while (!printQueue.isEmpty()) {
String job = printQueue.poll();
System.out.println("Printing: " + job);
}
System.out.println("Queue empty? " + printQueue.isEmpty());
}
}
Output:
Queue: [Report.pdf, Invoice.docx, Photo.png]
Next to print (peek): Report.pdf
Printing: Report.pdf
Printing: Invoice.docx
Printing: Photo.png
Queue empty? true
Each offer appends to the tail. peek shows the head (the oldest item) without removing it. The loop repeatedly polls the head until the queue is empty, printing jobs in the exact order they were added — that’s FIFO behavior in action.
Example 2: Deque as a Stack (Undo History)
import java.util.ArrayDeque;
import java.util.Deque;
public class Main {
public static void main(String[] args) {
Deque<String> undoStack = new ArrayDeque<>();
undoStack.push("Type 'Hello'");
undoStack.push("Type ' World'");
undoStack.push("Bold text");
System.out.println("Undo stack: " + undoStack);
System.out.println("Undo: " + undoStack.pop());
System.out.println("Undo: " + undoStack.pop());
System.out.println("Remaining actions: " + undoStack);
}
}
Output:
Undo stack: [Bold text, Type ' World', Type 'Hello']
Undo: Bold text
Undo: Type ' World'
Remaining actions: [Type 'Hello']
push inserts at the front, so the most recently pushed action ends up first in the deque. pop removes from the front too, giving classic LIFO (last-in-first-out) behavior — exactly what an undo stack needs. Notice this is the same ArrayDeque object being used as a stack, not a queue.
Example 3: Using Both Ends — Palindrome Check
import java.util.ArrayDeque;
import java.util.Deque;
public class Main {
static boolean isPalindrome(String text) {
Deque<Character> deque = new ArrayDeque<>();
for (char c : text.toCharArray()) {
deque.addLast(c);
}
while (deque.size() > 1) {
if (!deque.pollFirst().equals(deque.pollLast())) {
return false;
}
}
return true;
}
public static void main(String[] args) {
String[] words = {"level", "hello", "racecar"};
for (String word : words) {
System.out.println(word + " -> " + isPalindrome(word));
}
}
}
Output:
level -> true
hello -> false
racecar -> true
This is where a deque truly shines: it compares characters from both ends inward, popping from the front with pollFirst and from the back with pollLast in the same loop. A plain Queue couldn’t do this efficiently because it only exposes one end for removal.
Under the Hood
When you call offer on an ArrayDeque, the JVM writes the new element into the backing array at the tail index, then advances tail by one, wrapping back to index 0 if it runs past the array’s end (that’s the "circular" part). Removing from the head does the same at the head index. If head and tail ever meet because the array is full, ArrayDeque allocates a new array (typically double the capacity), copies all elements into it in logical order starting at index 0, and resets the indices — an O(n) operation that happens rarely (amortized O(1) per insertion, just like ArrayList growth).
A LinkedList, by contrast, allocates a new Node object for every element, containing the value and two references (prev and next). Adding or removing at either end simply rewires a couple of references — no resizing or copying — but each element now costs noticeably more memory, and because nodes live at scattered heap addresses, iterating a LinkedList is slower in practice than iterating the contiguous array inside an ArrayDeque, even though both are "O(1) per operation" in Big-O terms.
Common Mistakes
Mistake 1: Using the exception-throwing methods on a possibly-empty queue
remove() and element() throw NoSuchElementException when the queue is empty, which surprises people coming from other languages. Prefer poll() and peek(), which return null instead of throwing.
import java.util.LinkedList;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
try {
int value = queue.remove();
System.out.println("Got: " + value);
} catch (java.util.NoSuchElementException e) {
System.out.println("Oops: remove() threw NoSuchElementException on an empty queue!");
}
Integer safeValue = queue.poll();
System.out.println("poll() returned: " + safeValue);
}
}
Output:
Oops: remove() threw NoSuchElementException on an empty queue!
poll() returned: null
Mistake 2: Storing null elements in an ArrayDeque
LinkedList permits null elements, but ArrayDeque explicitly forbids them — it uses null internally as a sentinel to mean "empty slot", so allowing real null values would make poll() ambiguous (is the queue empty, or did it really contain null?). Attempting to add null throws a NullPointerException.
import java.util.ArrayDeque;
import java.util.Deque;
public class Main {
public static void main(String[] args) {
Deque<String> deque = new ArrayDeque<>();
try {
deque.add(null);
} catch (NullPointerException e) {
System.out.println("ArrayDeque rejects null elements!");
}
deque.add("A");
deque.add("B");
System.out.println("Deque contents: " + deque);
}
}
Output:
ArrayDeque rejects null elements!
Deque contents: [A, B]
A third common mistake is treating Queue as if it supports indexed access like a List. The Queue interface deliberately has no get(index) method — if you find yourself wanting random access, you probably want an ArrayList or ArrayDeque used differently, not a Queue.
Best Practices
- Declare variables with the interface type (
Queue<E>orDeque<E>), not the concrete class, so you can swap implementations later without changing calling code. - Prefer
ArrayDequeoverLinkedListfor both queue and stack use cases — it’s faster, more memory-efficient, and the JDK docs recommend it explicitly. - Use the
offer/poll/peekfamily in normal control flow; reserveadd/remove/elementfor cases where an empty queue truly indicates a bug you want to surface as an exception. - Never store
nullin anArrayDeque— use a sentinel object orOptionalif you need to represent "no value". - Use a
Dequeinstead of the legacyStackclass for LIFO behavior;Stackis synchronized (slower) and extendsVector, which exposes indexed mutation that breaks stack discipline. - For breadth-first search (BFS) over graphs or trees, use a
Queue(usuallyArrayDeque) to hold the frontier of nodes to visit next.
Practice Exercises
- Exercise 1: Write a method
reverseWithStack(Deque<Integer> input)that uses a secondDequeas a stack (viapush/pop) to reverse the order of elements ininput, printing the reversed list. - Exercise 2: Simulate a hot-potato / round-robin scheduler: put five player names into a
Queue, then repeatedly poll a name, print it, and offer it back to the tail three times each, so each player gets three turns in order. - Exercise 3: Using a
Deque<Integer>, implement a sliding-window maximum for the array{1, 3, -1, -3, 5, 3, 6, 7}with window size 3 (hint: keep indices in the deque, dropping from the back whenever a smaller value is found).
Summary
Queuemodels FIFO (first-in-first-out) behavior;Dequeextends it to allow insertion and removal at both ends.ArrayDeque(circular array) is generally faster and more memory-efficient thanLinkedList(doubly linked nodes) for both queue and stack use.- Each core operation has a throwing form (
add,remove,element) and a sentinel-returning form (offer,poll,peek) — prefer the sentinel form for normal control flow. - A
Dequecan act as a stack viapush/pop, replacing the legacy, synchronizedStackclass. ArrayDequedoes not allownullelements;LinkedListdoes.- Deques are the natural tool whenever you need to work from both ends of a sequence, such as palindrome checks or sliding-window algorithms.
