Deque is a double-ended queue that allows adding/removing from both ends.
ArrayDeque uses a circular resizable array — no per-element overhead.
Use as stack: push()/pop() (LIFO). Use as queue: offerLast()/pollFirst() (FIFO).
ArrayDeque is 3-5x faster than LinkedList for head/tail operations due to cache locality.
In production, LinkedList as a Deque causes GC pressure — ArrayDeque avoids node allocations.
Biggest mistake: assuming LinkedList is better because it's "linked" — it's not for Deque use.
Plain-English First
Imagine a tube of tennis balls. You can push a ball in from either end and pop one out from either end — it's not a one-way street like a normal queue at the supermarket. That tube is a Deque (double-ended queue). Java's ArrayDeque is just a very efficient version of that tube, built from a resizable array instead of individually chained nodes.
Most Java developers reach for ArrayList or LinkedList without thinking twice. But the moment you need a stack, a queue, or anything where you add or remove from both ends, you're using the wrong tool — and paying for it in performance and readability. Deque (pronounced 'deck') is Java's purpose-built answer to this exact problem, and it's been quietly sitting in the standard library since Java 6.
The classic pain point looks like this: you want a stack, so you use java.util.Stack. It works — but Stack extends Vector, which is synchronized, which means every single push and pop acquires a lock even when you're on a single thread. That's unnecessary overhead, and the Java docs themselves tell you to prefer Deque over Stack. ArrayDeque solves this: no synchronization overhead, no legacy baggage, and faster iteration because the data lives in a contiguous array rather than scattered heap nodes.
By the end of this article you'll understand what Deque actually is (not just what it does), why ArrayDeque is the go-to implementation for most use cases, how to use it as both a stack and a queue in the same codebase, and exactly what traps to avoid. You'll also have a concrete mental model you can pull out in interviews.
What Deque Actually Is — and Why It Replaces Both Stack and Queue
The Deque interface extends Queue and adds mirror-image methods for the front of the collection. Where a plain Queue only lets you offer() at the tail and poll() from the head, a Deque lets you do both operations at both ends. This single property makes it flexible enough to act as a stack (LIFO) or a queue (FIFO) depending on which pair of methods you call.
This isn't just an academic curiosity. Real problems need it. Browser history is a deque — new pages push to the front, the back button pops from the front, and you can cap the history size by dropping from the tail. A sliding-window maximum algorithm in competitive programming uses a deque to efficiently track the largest element in a moving window. An undo/redo system uses it to maintain two stacks in one structure.
The Deque interface gives you three styles of every operation: one that throws an exception on failure (addFirst, removeFirst), one that returns a special value (offerFirst, pollFirst), and one that blocks — but only in the BlockingDeque sub-interface. For ArrayDeque, stick to the offer/poll family so your code stays null-and-exception-safe.
The key mental shift: stop thinking of it as 'a fancy list' and start thinking of it as 'a double-ended conveyor belt with defined entry and exit points at each end.'
DequeAsStackAndQueue.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package io.thecodeforge.deque;
import java.util.ArrayDeque;
import java.util.Deque;
publicclassDequeAsStackAndQueue {
publicstaticvoidmain(String[] args) {
// ── Using ArrayDeque as a STACK (LIFO) ──────────────────────────// Think: a stack of pancakes — last one placed is first one eaten.Deque<String> browserHistory = newArrayDeque<>();
browserHistory.push("google.com"); // pushes to the FRONT (head)
browserHistory.push("github.com"); // pushes to the FRONT, google moves back
browserHistory.push("thecodeforge.io"); // now this is at the topSystem.out.println("=== Browser History (Stack) ===");
System.out.println("Current page : " + browserHistory.peek()); // look without removingSystem.out.println("Going back : " + browserHistory.pop()); // removes from frontSystem.out.println("Now on : " + browserHistory.peek());
// ── Using ArrayDeque as a QUEUE (FIFO) ──────────────────────────// Think: a printer queue — first document sent is first printed.Deque<String> printQueue = newArrayDeque<>();
printQueue.offerLast("invoice.pdf"); // add to the TAIL
printQueue.offerLast("report.docx"); // add to the TAIL
printQueue.offerLast("photo.png"); // add to the TAILSystem.out.println("\n=== Print Queue (FIFO) ===");
// pollFirst() removes from the HEAD — first in, first outwhile (!printQueue.isEmpty()) {
System.out.println("Printing: " + printQueue.pollFirst());
}
// ── Using BOTH ends simultaneously ───────────────────────────────// A sliding window of the last 3 user actionsDeque<String> recentActions = newArrayDeque<>();
String[] actions = {"login", "view-dashboard", "edit-profile", "upload-photo", "logout"};
int windowSize = 3;
System.out.println("\n=== Last " + windowSize + " Actions ===");
for (String action : actions) {
recentActions.offerLast(action); // new action enters at the tailif (recentActions.size() > windowSize) {
recentActions.pollFirst(); // oldest action drops from the head
}
}
System.out.println("Recent actions: " + recentActions);
}
}
Declare your variable as Deque<T>, not ArrayDeque<T>. Program to the interface. If you later need a thread-safe version, you can swap in LinkedBlockingDeque without touching any other line of code.
Production Insight
In production, declaring Deque<T> instead of ArrayDeque<T> lets you swap implementations without changing consumer code.
Deque as an interface also makes testing easier — you can inject a mock.
Rule: always code to the interface, not the concrete class.
Key Takeaway
Deque is a double-ended queue that extends Queue.
Use it for both stack (LIFO) and queue (FIFO) operations.
ArrayDeque is the go-to implementation for single-threaded use.
Why ArrayDeque Beats Stack, LinkedList, and Even ArrayList for This Job
ArrayDeque is backed by a circular array — a plain Object[] where head and tail are just integer pointers that wrap around. When you push to the front, the head pointer moves left (mod capacity). When you offer to the back, the tail pointer moves right. No object allocation per element, no pointer chasing, no garbage to collect. This gives O(1) amortized time for addFirst, addLast, removeFirst, and removeLast.
Compare that to LinkedList: every node is a separate heap object with two reference fields. That's 24–32 bytes of overhead per element on a modern JVM, scattered across memory, which destroys CPU cache locality. LinkedList is theoretically O(1) for head/tail operations too, but the constant factor is much higher and GC pressure is real.
Stack is even worse for new code. It inherits from Vector, which wraps every method in synchronized — meaning you pay for thread safety you don't need on a single thread. The Java 11 docs literally say: 'A more complete and consistent set of LIFO stack operations is provided by the Deque interface and its implementations, which should be used in preference to this class.'
ArrayDeque does have one catch: it doesn't allow null elements. This is actually a feature — null is used as a sentinel value internally for certain checks, and if you store nulls, poll() returning null becomes ambiguous (empty queue, or null element?). If you genuinely need to store nulls, LinkedList is the fallback.
ArrayDequeInternals.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package io.thecodeforge.deque;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Stack;
publicclassArrayDequeInternals {
// A simple benchmark to make the performance difference tangible.// Not a rigorous JMH benchmark, but good enough to see the shape.staticfinalintOPERATIONS = 1_000_000;
publicstaticvoidmain(String[] args) {
// ── ArrayDeque timing ────────────────────────────────────────────Deque<Integer> arrayDeque = new ArrayDeque<>(OPERATIONS); // pre-size to avoid resizinglong start = System.nanoTime();
for (int i = 0; i < OPERATIONS; i++) {
arrayDeque.push(i); // addFirst under the hood
}
for (int i = 0; i < OPERATIONS; i++) {
arrayDeque.pop(); // removeFirst under the hood
}
long arrayDequeMs = (System.nanoTime() - start) / 1_000_000;
// ── LinkedList timing ────────────────────────────────────────────Deque<Integer> linkedList = newLinkedList<>();
start = System.nanoTime();
for (int i = 0; i < OPERATIONS; i++) {
linkedList.push(i);
}
for (int i = 0; i < OPERATIONS; i++) {
linkedList.pop();
}
long linkedListMs = (System.nanoTime() - start) / 1_000_000;
// ── Legacy Stack timing ──────────────────────────────────────────Stack<Integer> legacyStack = newStack<>();
start = System.nanoTime();
for (int i = 0; i < OPERATIONS; i++) {
legacyStack.push(i);
}
for (int i = 0; i < OPERATIONS; i++) {
legacyStack.pop();
}
long stackMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("=== 1,000,000 push + pop operations ===");
System.out.printf("ArrayDeque : %d ms%n", arrayDequeMs);
System.out.printf("LinkedList : %d ms%n", linkedListMs);
System.out.printf("Stack : %d ms%n", stackMs);
// ── Null safety demonstration ────────────────────────────────────System.out.println("\n=== Null element behaviour ===");
try {
Deque<String> noNulls = newArrayDeque<>();
noNulls.offerLast(null); // ArrayDeque rejects null
} catch (NullPointerException e) {
// ArrayDeque throws NPE rather than silently storing nullSystem.out.println("ArrayDeque: null rejected — " + e.getClass().getSimpleName());
}
Deque<String> nullsOk = newLinkedList<>();
nullsOk.offerLast(null); // LinkedList accepts nullSystem.out.println("LinkedList : null accepted, poll() returns: " + nullsOk.pollFirst());
// Ambiguous! Is null because the queue had a null element, or because it's empty?System.out.println("LinkedList : poll() on empty returns: " + nullsOk.pollFirst());
// Both print null — you can't tell the difference.
}
}
Output
=== 1,000,000 push + pop operations ===
ArrayDeque : 18 ms
LinkedList : 74 ms
Stack : 91 ms
=== Null element behaviour ===
ArrayDeque: null rejected — NullPointerException
LinkedList : null accepted, poll() returns: null
LinkedList : poll() on empty returns: null
Watch Out:
Actual timings vary by JVM warmup and hardware. The relative shape (ArrayDeque fastest, Stack slowest) is consistent across machines. For production-critical benchmarking, always use JMH — never raw System.nanoTime() in a loop.
Production Insight
In production, the constant factor matters more than Big-O.
A LinkedList's node allocation can cause 10x more GC pressure than ArrayDeque's array storage.
Rule: for Deque, ArrayDeque is almost always the right choice in single-threaded code.
Key Takeaway
ArrayDeque outperforms Stack (no sync overhead) and LinkedList (no node allocation).
Null rejection is a feature — it avoids ambiguity in poll() return values.
Pre-size ArrayDeque for best performance when you know the expected capacity.
Real-World Pattern: Undo/Redo with Two Deques
The undo/redo pattern is one of the clearest real-world demonstrations of why Deque exists. Most developers reach for two Stacks — but since ArrayDeque replaces Stack, you end up with two ArrayDeques. The elegance here is in the data flow: every action pushes to the undo deque. An undo pops from the undo deque and pushes to the redo deque. A redo pops from the redo deque and pushes back to the undo deque. Any brand-new action clears the redo deque, because you've forked off the history.
This pattern appears everywhere: text editors, Photoshop, database transaction rollback, Git (roughly), and any wizard-style form with a Back button.
The example below models a simple text editor command history. Notice how the deques' push/pop semantics make the intent obvious at the call site — you're not juggling indices or size checks. The code reads almost like a specification of the algorithm.
UndoRedoEditor.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package io.thecodeforge.deque;
import java.util.ArrayDeque;
import java.util.Deque;
// Simulates a minimal text editor with undo/redo support.publicclassUndoRedoEditor {
private final Deque<String> undoStack = new ArrayDeque<>(); // commands we can undo
private final Deque<String> redoStack = new ArrayDeque<>(); // commands we can redoprivatefinalStringBuilder document = newStringBuilder();
// Performs a new edit and records it for potential undopublicvoidperformEdit(String command, String text) {
String snapshot = document.toString(); // save state BEFORE the changeapplyEdit(command, text);
undoStack.push(snapshot); // push old state so undo can restore it
redoStack.clear(); // branching — future redo history is now invalidSystem.out.println("[EDIT] command='" + command + "' text='" + text
+ "' | doc now: '" + document + "'");
}
// Undoes the last editpublicvoidundo() {
if (undoStack.isEmpty()) {
System.out.println("[UNDO] nothing to undo");
return;
}
redoStack.push(document.toString()); // save current state so redo can restore itString previousState = undoStack.pop(); // restore the state before last edit
document.setLength(0);
document.append(previousState);
System.out.println("[UNDO] doc now: '" + document + "'");
}
// Redoes the last undone editpublicvoidredo() {
if (redoStack.isEmpty()) {
System.out.println("[REDO] nothing to redo");
return;
}
undoStack.push(document.toString()); // save current so we can undo the redoString redoState = redoStack.pop();
document.setLength(0);
document.append(redoState);
System.out.println("[REDO] doc now: '" + document + "'");
}
privatevoidapplyEdit(String command, String text) {
switch (command) {
case"append" -> document.append(text);
case"delete" -> {
int len = Math.min(text.length(), document.length());
document.delete(document.length() - len, document.length());
}
default -> thrownewIllegalArgumentException("Unknown command: " + command);
}
}
publicstaticvoidmain(String[] args) {
UndoRedoEditor editor = newUndoRedoEditor();
editor.performEdit("append", "Hello");
editor.performEdit("append", ", World");
editor.performEdit("append", "!");
System.out.println();
editor.undo(); // removes '!'
editor.undo(); // removes ', World'System.out.println();
editor.redo(); // restores ', World'System.out.println();
// Performing a new edit here clears redo history
editor.performEdit("append", " from Java");
editor.redo(); // nothing to redo — new branch forked the history
}
}
[EDIT] command='append' text=' from Java' | doc now: 'Hello, World from Java'
[REDO] nothing to redo
Interview Gold:
If asked to design undo/redo in an interview, start by saying 'I'd use two ArrayDeques acting as stacks.' Then explain the branching rule — that a new action clears the redo stack. That detail alone separates candidates who've built real features from those who've just memorised data structure definitions.
Production Insight
In a real editor, memory can blow up if you keep full document snapshots.
Consider storing diffs instead of full snapshots to bound memory.
Rule: understand the trade-off between simplicity and memory before shipping.
Key Takeaway
Undo/redo is the classic two-deque pattern.
New action clears redo history — that's the branching rule.
ArrayDeque gives you O(1) push/pop for both stacks.
Every Deque Method You'll Actually Use — and Which to Avoid
ArrayDeque has a lot of methods and the naming is genuinely confusing at first, because the same logical operation has three names depending on the error-handling style you want. Let's demystify this once and for all.
The pattern is: each end (First/Last) has three method styles — throwing, returning-null/false, and the legacy push/pop/peek aliases (which always operate on the head). The throwing variants (addFirst, removeFirst) raise NoSuchElementException on an empty deque. The safe variants (offerFirst, pollFirst, peekFirst) return null or false without throwing. Use the safe variants by default unless an empty deque is a programming error you genuinely want to crash on.
The push(), pop(), and peek() methods are Stack compatibility aliases. push(e) is addFirst(e). pop() is removeFirst(). peek() is peekFirst(). They're fine to use, but mixing them with offerLast() in the same class can confuse readers about which end you're operating on. A clean codebase picks a convention and sticks to it: either always use push/pop (stack style) or always use offerFirst/offerLast/pollFirst/pollLast (explicit style).
DequeMethodReference.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package io.thecodeforge.deque;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.NoSuchElementException;
publicclassDequeMethodReference {
publicstaticvoidmain(String[] args) {
Deque<String> taskQueue = newArrayDeque<>();
// ── Adding elements ──────────────────────────────────────────────
taskQueue.offerFirst("low-priority-task"); // safe add to HEAD
taskQueue.offerLast("normal-task"); // safe add to TAIL
taskQueue.offerFirst("urgent-task"); // jumps to the front// taskQueue is now: [urgent-task, low-priority-task, normal-task]System.out.println("Queue: " + taskQueue);
// ── Peeking (non-destructive) ────────────────────────────────────System.out.println("Nextup (head): " + taskQueue.peekFirst()); // does NOT removeSystem.out.println("Lastin (tail): " + taskQueue.peekLast()); // does NOT removeSystem.out.println("Still intact : " + taskQueue); // unchanged// ── Removing elements ────────────────────────────────────────────String nextTask = taskQueue.pollFirst(); // safe remove from HEAD, returns null if emptySystem.out.println("Processing: " + nextTask);
System.out.println("Remaining : " + taskQueue);
// ── Throwing vs safe variants ────────────────────────────────────Deque<String> emptyDeque = newArrayDeque<>();
// Safe — returns null, no exceptionString safeResult = emptyDeque.pollFirst();
System.out.println("\npollFirst on empty: " + safeResult); // null// Throwing — raises NoSuchElementExceptiontry {
emptyDeque.removeFirst(); // throws because the deque is empty
} catch (NoSuchElementException e) {
System.out.println("removeFirst on empty: " + e.getClass().getSimpleName());
}
// ── Stack aliases (push/pop operate on the HEAD) ─────────────────Deque<Integer> callStack = newArrayDeque<>();
callStack.push(1); // same as addFirst(1)
callStack.push(2); // same as addFirst(2)
callStack.push(3); // same as addFirst(3)System.out.println("\nCall stack top: " + callStack.peek()); // 3 — same as peekFirst()System.out.println("Popped : " + callStack.pop()); // 3 — same as removeFirst()System.out.println("New top : " + callStack.peek()); // 2// ── Size checks ──────────────────────────────────────────────────System.out.println("\nSize : " + callStack.size());
System.out.println("Empty? : " + callStack.isEmpty());
System.out.println("Contains1? : " + callStack.contains(1)); // O(n) linear scan
}
}
Still intact : [urgent-task, low-priority-task, normal-task]
Processing: urgent-task
Remaining : [low-priority-task, normal-task]
pollFirst on empty: null
removeFirst on empty: NoSuchElementException
Call stack top: 3
Popped : 3
New top : 2
Size : 2
Empty? : false
Contains 1? : true
Pro Tip:
Use contains() sparingly — it's O(n) because ArrayDeque isn't a Set. If you find yourself calling contains() frequently on a deque, you probably want a LinkedHashSet or a supplementary Set alongside your deque to track membership in O(1).
Production Insight
In production, calling contains() on a large ArrayDeque can cause unexpected latency spikes.
Always consider the O(n) cost before using contains() in a hot path.
Rule: if you need fast membership checks, use a separate HashSet.
Key Takeaway
Deque has three method styles: throwing, safe (null-returning), and stack-aliases.
Use safe variants (offer/poll/peek) by default to avoid exceptions.
Stick to one convention per deque — don't mix push with pollLast.
Performance Tuning: Pre-allocating Capacity and Avoiding Resizes
ArrayDeque's underlying array doubles in size when it runs out of capacity. This resize operation involves allocating a new array and copying all existing references — an O(n) operation. If you know roughly how many elements your deque will hold, you can pre-allocate the capacity in the constructor and avoid resizes altogether.
For example, if you're using a deque as a work queue that processes 10,000 tasks before being drained, create it with new ArrayDeque<>(10_000). This eliminates all resizing overhead and ensures consistent push/pop latency.
But there's a trade-off: the array never shrinks unless you call trimToSize() (ArrayDeque doesn't have this method — you'd have to create a new deque and copy elements). If you allocate too large, you waste memory. For bounded usage patterns (like sliding windows), the waste is negligible. For unbounded queues, the array naturally grows as needed.
Pre-sizing also improves cache locality because the array is exactly the right size from the start, avoiding the scattered memory of a partially grown array.
ArrayDequePreSize.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package io.thecodeforge.deque;
import java.util.ArrayDeque;
import java.util.Deque;
publicclassArrayDequePreSize {
publicstaticvoidmain(String[] args) {
finalintOPERATIONS = 1_000_000;
// ── Without pre-sizing (starts at capacity 16, resizes multiple times) ──Deque<Integer> noPresize = newArrayDeque<>();
long start = System.nanoTime();
for (int i = 0; i < OPERATIONS; i++) {
noPresize.offerLast(i);
}
for (int i = 0; i < OPERATIONS; i++) {
noPresize.pollFirst();
}
long noPresizeMs = (System.nanoTime() - start) / 1_000_000;
// ── With pre-sizing (exact capacity) ─────────────────────────────Deque<Integer> presize = newArrayDeque<>(OPERATIONS);
start = System.nanoTime();
for (int i = 0; i < OPERATIONS; i++) {
presize.offerLast(i);
}
for (int i = 0; i < OPERATIONS; i++) {
presize.pollFirst();
}
long presizeMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("=== 1,000,000 offer/poll operations ===");
System.out.printf("Without pre-sizing: %d ms (includes %d resizes)%n", noPresizeMs, (int)(Math.log(OPERATIONS/16)/Math.log(2)));
System.out.printf("With pre-sizing : %d ms%n", presizeMs);
System.out.println("Pre-sizing eliminates resize overhead — especially noticeable with large capacities.");
}
}
Output
=== 1,000,000 offer/poll operations ===
Without pre-sizing: 32 ms (includes 16 resizes)
With pre-sizing : 24 ms
Pre-sizing eliminates resize overhead — especially noticeable with large capacities.
When to Pre-size:
Always pre-size if you know the expected number of elements. The capacity must be at least the expected size. If you're unsure, a safe overestimate is better than no pre-sizing — the memory waste from a few hundred extra slots is negligible compared to the cost of multiple resizes.
Production Insight
In high-throughput systems, resizing can cause latency spikes as the JVM allocates and copies arrays.
Pre-sizing at construction time avoids this entirely.
Rule: if you know the load, pre-size the ArrayDeque.
Use new ArrayDeque<>(expectedSize) when you know the load.
For bounded usage, pre-sizing also improves memory and latency predictability.
● Production incidentPOST-MORTEMseverity: high
LinkedList as Deque Caused 10-Second GC Pauses in a Logging Service
Symptom
Application experienced frequent GC pauses (up to 10 seconds) during peak load. Heap dump analysis showed millions of LinkedList$Node objects occupying >80% of heap.
Assumption
LinkedList's O(1) insert/remove at both ends would be fast enough for the logging queue — after all, Big-O says it's linear time.
Root cause
LinkedList allocates a new Node object for every element. Under 50,000 log lines per second, this created massive allocation pressure, prematurely triggering Full GCs. The nodes scattered across memory also destroyed CPU cache performance.
Fix
Replaced LinkedList with ArrayDeque (pre-sized to expected capacity). Push operations became allocation-free (except occasional array resize), and sequential access became cache-friendly. GC pauses dropped to <100ms.
Key lesson
Big-O hides constant factors — LinkedList's O(1) is cheap per operation but expensive at scale due to allocation overhead.
ArrayDeque is almost always the right default for Deque usage in single-threaded code.
Pre-size ArrayDeque when you know the expected load to avoid resize copies.
Production debug guideCommon failure patterns and their quick fixes when using ArrayDeque.4 entries
Symptom · 01
Deque returns null when you expect a value
→
Fix
Check whether you used pollFirst() or pollLast() on an empty deque. poll returns null for empty deque; remove throws NoSuchElementException. Verify the deque isn't being cleared elsewhere (e.g., by clear() or draining consumers).
Symptom · 02
ConcurrentModificationException while iterating
→
Fix
ArrayDeque is not thread-safe. If you iterate while another thread modifies the deque, you'll get ConcurrentModificationException. Use SynchronizedDeque or LinkedBlockingDeque for multi-threaded access.
Symptom · 03
ArrayDeque grows unexpectedly large
→
Fix
Check that you're not inadvertently using push() instead of offerLast() if you want FIFO. Also verify that you're actually removing elements — a leak in the consumer can cause unbounded growth. Use profiling to see if deque size keeps increasing.
Symptom · 04
NullPointerException from addFirst or offerLast
→
Fix
ArrayDeque does not allow null elements. Wrap your data in Optional<T> or use a sentinel value. If you must store nulls, switch to LinkedList (but measure the performance cost).
★ ArrayDeque Debugging Quick ReferenceCommon symptoms and immediate commands to diagnose Deque-related problems in a running JVM.
High memory usage / GC pressure suspected from Deque usage−
Immediate action
Take a heap dump and inspect the Deque implementation class.
Commands
jcmd <pid> GC.heap_dump /tmp/dump.hprof
Use Eclipse MAT to search for java.util.LinkedList to confirm usage.
Fix now
Replace LinkedList with ArrayDeque and ensure pre-sizing with new ArrayDeque<>(expectedCapacity).
Deque gradually fills and never shrinks (memory leak)+
Immediate action
Check for missing poll() calls in consumer threads. Enable logging around push/offer and poll.
Commands
jstack <pid> to see thread stacks — look for threads stuck in offers without corresponding polls.
Use a watch on the deque size via JMX or add temporary size logging.
Fix now
Add a bounded cap: if (deque.size() > MAX) { deque.pollLast(); } to evict oldest.
Unexpected ordering when using push and pollLast+
Immediate action
Verify which end each operation uses. push() operates on head, pollLast() on tail.
Commands
Check the code for mixed usage of both head and tail methods on the same deque.
Add a temporary toString() on the deque to verify order.
Fix now
Standardise: use only push/pop for stack usage, or only offerLast/pollFirst for queue usage in a single deque.
ArrayDeque vs LinkedList vs Stack
Feature / Aspect
ArrayDeque
LinkedList (as Deque)
Stack (legacy)
Underlying structure
Circular resizable array
Doubly-linked nodes
Resizable array (via Vector)
Memory per element
~4-8 bytes (reference only)
~32-40 bytes (node + 2 refs)
~4-8 bytes (reference only)
addFirst / addLast
O(1) amortized
O(1) true
O(1) amortized (tail only)
removeFirst / removeLast
O(1) amortized
O(1) true
O(1) top only, O(n) bottom
Random access (get by index)
Not supported
O(n)
O(1) via inherited Vector
Thread-safe?
No
No
Yes (synchronized — costly)
Allows null elements?
No — throws NPE
Yes
Yes
Iteration order
Head to tail (FIFO)
Head to tail (FIFO)
Bottom to top (LIFO)
Recommended for stack?
Yes — preferred
Acceptable
No — legacy, avoid
Recommended for queue?
Yes — preferred
Acceptable
No — wrong structure
Java version introduced
Java 6
Java 1.2
Java 1.0
Key takeaways
1
ArrayDeque is the right default for both stacks and queues in single-threaded Java
it outperforms Stack (no synchronization) and LinkedList (better cache locality) in almost every benchmark.
2
Deque's method table is systematic, not arbitrary
each end (First/Last) has a throwing variant and a safe variant. Learn the pattern once and you know all 8 methods.
3
ArrayDeque does not allow null elements by design
null is ambiguous as a return value from pollFirst() because it could mean 'empty' or 'stored null'. This is a feature, not a limitation.
4
The undo/redo pattern is the canonical real-world demonstration of Deque
two ArrayDeques acting as stacks, with the branching rule (new action clears redo) being the detail that proves you've actually used it.
5
Pre-size ArrayDeque when you know the expected capacity to avoid resize overhead. It's a simple change that can significantly improve latency predictability.
Common mistakes to avoid
4 patterns
×
Using Stack instead of ArrayDeque for LIFO operations
Symptom
Unnecessarily slower code and mixing synchronized methods with single-threaded logic; the Java docs explicitly deprecate this usage pattern.
Fix
Replace new Stack<>() with new ArrayDeque<>() and use push()/pop()/peek(); no other code changes needed.
×
Storing null elements in ArrayDeque
Symptom
NullPointerException thrown at offerLast(null) or addFirst(null), which is confusing if you're used to ArrayList accepting nulls.
Fix
If your data can genuinely be null, use Optional<T> as your element type (e.g., Deque<Optional<String>>) so absence of a value is explicit, not ambiguous.
×
Mixing push/pop (head-only) with offerLast/pollLast (tail) in the same class
Symptom
Logic bugs where elements are removed from the wrong end; push() adds to head but pollLast() removes from tail, so what looks like a stack behaves like a queue.
Fix
Pick one convention per Deque instance. Document it with a comment at the declaration site, e.g., // used as STACK — always push/pop from head only.
×
Not pre-sizing ArrayDeque when expected capacity is known
Symptom
Multiple resize operations cause latency spikes and increased GC overhead during high load.
Fix
Use new ArrayDeque<>(expectedCapacity) at construction time. An overestimate is safer than no pre-sizing.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Why does the Java documentation recommend using ArrayDeque over Stack fo...
Q02JUNIOR
Explain the difference between the throwing methods (addFirst, removeFir...
Q03SENIOR
ArrayDeque is backed by a circular array. What does 'circular' mean in t...
Q01 of 03SENIOR
Why does the Java documentation recommend using ArrayDeque over Stack for LIFO operations, and what is the specific cost of using Stack in a single-threaded application?
ANSWER
Stack extends Vector, which synchronizes every method. In single-threaded code, that adds unnecessary lock acquisition overhead. ArrayDeque has no synchronization, is faster due to cache locality, and provides the same push/pop/peek methods. The performance difference can be 3-5x for high-throughput workloads.
Q02 of 03JUNIOR
Explain the difference between the throwing methods (addFirst, removeFirst) and the safe methods (offerFirst, pollFirst) in the Deque interface. When would you choose one over the other?
ANSWER
Throwing methods throw NoSuchElementException when the deque is empty. Safe methods return null or false. Use safe methods by default to avoid exceptions in normal flow. Use throwing methods when an empty deque at that point is a programming error that should crash fast (fail-fast behaviour).
Q03 of 03SENIOR
ArrayDeque is backed by a circular array. What does 'circular' mean in this context, and why does it allow O(1) addFirst without shifting every element — something a regular array can't do?
ANSWER
A circular array uses head and tail integer pointers that wrap around modulo the array length. addFirst moves the head pointer left (mod capacity) and writes at the new head position — no shifting required. Similarly, addLast moves the tail pointer right. Only when the array is full does it double in size (amortized O(1)). Without the circular wrap, adding to the front would require O(n) shifts.
01
Why does the Java documentation recommend using ArrayDeque over Stack for LIFO operations, and what is the specific cost of using Stack in a single-threaded application?
SENIOR
02
Explain the difference between the throwing methods (addFirst, removeFirst) and the safe methods (offerFirst, pollFirst) in the Deque interface. When would you choose one over the other?
JUNIOR
03
ArrayDeque is backed by a circular array. What does 'circular' mean in this context, and why does it allow O(1) addFirst without shifting every element — something a regular array can't do?
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
Is ArrayDeque thread-safe in Java?
No, ArrayDeque is not thread-safe. If multiple threads access it concurrently, you must synchronize externally (e.g., Collections.synchronizedDeque()) or use a thread-safe alternative like LinkedBlockingDeque from java.util.concurrent. For single-threaded code, ArrayDeque is the preferred choice precisely because it avoids synchronization overhead.
Was this helpful?
02
What is the difference between Deque and Queue in Java?
Queue is a one-ended interface — you add at the tail and remove from the head (FIFO only). Deque extends Queue and adds mirror methods for the head, so you can add and remove from either end. Every Deque is a Queue, but not every Queue is a Deque. Use Queue when FIFO semantics are all you need; use Deque when you need stack behaviour, double-ended access, or both.
Was this helpful?
03
Why does ArrayDeque say it's faster than LinkedList if both are O(1) for head/tail operations?
Big-O notation hides constant factors. LinkedList's O(1) operations still allocate a new Node object per element, which means GC pressure and scattered heap memory that destroys CPU cache performance. ArrayDeque stores references in a contiguous array, so traversal and sequential access are cache-friendly. In practice, ArrayDeque is 3–5x faster for push/pop workloads because of this memory layout difference.
Was this helpful?
04
How do I choose between ArrayDeque and LinkedList when I need to store null elements?
If you must store nulls, LinkedList is the only Deque implementation that allows it. However, consider using Optional<T> with ArrayDeque instead — the performance benefits usually outweigh the inconvenience. If you choose LinkedList, be aware of the memory overhead and GC pressure, especially at scale.