Java ArrayDeque
Java ArrayDeque
java.util.ArrayDeque<E> is the collection the Java documentation explicitly recommends over both java.util.Stack and java.util.LinkedList for all queue and stack use cases. It is a concrete implementation of the Deque interface backed by a resizable circular array. No per-element object allocation, no synchronisation overhead, no wasted traversals for head operations — just direct array index arithmetic at both ends.
Every time a senior developer reviews code and replaces new Stack<>() with new ArrayDeque<>(), this is why.
What Is Java ArrayDeque?
ArrayDeque<E> is a concrete class in java.util that implements Deque<E>. It stores elements in a single resizable Object[] array using a circular buffer with two integer pointers — head and tail — that wrap around the array boundaries. This structure gives O(1) amortised operations at both ends simultaneously without the pointer-chasing penalty of LinkedList.
The diagram below shows where ArrayDeque sits in the Collections hierarchy.
java.lang.Iterable<E>
└── java.util.Collection<E>
└── java.util.Queue<E> ← FIFO contract
└── java.util.Deque<E> ← double-ended: both-end access
├── ArrayDeque ← THIS CLASS: circular resizable array
└── LinkedList ← doubly-linked nodes, also implements List
KEY FACTS:
Package : java.util
Since : Java 1.6
Implements : Deque<E>, Cloneable, Serializable
Backing store : Object[] with circular head/tail indices
Null : NOT allowed — NullPointerException on any null insertion
Duplicates : allowed
Thread-safe : NO — use LinkedBlockingDeque for concurrent access
Default capacity: 16 elements (internal — not configurable in the same way as ArrayList)
Resize strategy : approximately double, always a power of 2
Basic Overview — The Full ArrayDeque API
ArrayDeque exposes every method in the Deque interface plus a few additions. All operations at both ends are O(1) amortised.
ARRAYDEQUE COMPLETE METHOD REFERENCE:
HEAD OPERATIONS (front of deque):
addFirst(e) / offerFirst(e) ← insert at head (throws / returns false)
removeFirst() / pollFirst() ← remove from head (throws / returns null)
getFirst() / peekFirst() ← inspect head (throws / returns null)
TAIL OPERATIONS (back of deque):
addLast(e) / offerLast(e) ← insert at tail (throws / returns false)
removeLast() / pollLast() ← remove from tail (throws / returns null)
getLast() / peekLast() ← inspect tail (throws / returns null)
STACK ALIASES (all operate on head):
push(e) = addFirst(e)
pop() = removeFirst()
peek() = peekFirst()
QUEUE ALIASES (standard Queue contract):
add(e) = addLast(e)
offer(e) = offerLast(e)
remove() = removeFirst()
poll() = pollFirst()
element() = getFirst()
peek() = peekFirst()
ITERATION:
iterator() ← head → tail (front to back)
descendingIterator() ← tail → head (back to front, unique to Deque)
SIZE:
size(), isEmpty(), clear(), contains(e), toArray()
PRODUCTION RULE:
Always prefer the null-returning variants:
offerFirst, offerLast, pollFirst, pollLast, peekFirst, peekLast
These never throw on empty — empty deque is a normal operating condition.
When to Use ArrayDeque
USE ArrayDeque WHEN:
1. A LIFO stack is needed — always prefer over java.util.Stack
Stack.push()/pop() acquire a mutex on every call (synchronized from Vector)
ArrayDeque.push()/pop() have zero synchronisation cost
2. A FIFO queue is needed — prefer over LinkedList
LinkedList allocates a Node object per element; ArrayDeque does not
ArrayDeque's contiguous array has better CPU cache locality
3. Both-end access is needed on the same collection
— sliding window algorithms, palindrome check, monotone deque
— work-stealing schedulers, undo-redo history with fixed-size window
4. Memory and GC pressure matter
— no per-element Node → smaller heap footprint
— fewer short-lived objects for GC to collect
CHOOSE LinkedList (as Deque) ONLY WHEN:
- Both Deque and List interfaces are needed on the same reference
- get(index) or set(index, e) or subList() required alongside deque operations
- Null elements must be stored (ArrayDeque rejects null; LinkedList allows it)
CHOOSE LinkedBlockingDeque WHEN:
- Multiple producer/consumer threads share the deque
- Blocking takeFirst()/takeLast() that wait on empty is needed
NEVER USE java.util.Stack IN NEW CODE:
- Synchronized on every operation even in single-threaded programs
- Inherits Vector's get(index)/set(index) which break stack encapsulation
- Java documentation explicitly recommends ArrayDeque as the replacement
How ArrayDeque Works Internally
The Circular Array and Head/Tail Mechanics
The core insight is that ArrayDeque treats its internal array as a circle. head and tail are integer indices that advance on opposite sides — head moves backward on addFirst (or forward on pollFirst), tail moves forward on addLast.
INITIAL STATE (empty ArrayDeque, capacity 16):
elements: [ null × 16 ]
head = 0, tail = 0
AFTER addLast("A"), addLast("B"), addLast("C"):
elements: [ "A" | "B" | "C" | null | null | ... | null ]
0 1 2 3 4 15
head = 0, tail = 3
AFTER addFirst("Z") — insert BEFORE head:
head = (0 - 1) & 15 = 15 ← bitwise wrap to last slot
elements[15] = "Z"
elements: [ "A" | "B" | "C" | null | ... | null | "Z" ]
0 1 2 3 14 15
head = 15, tail = 3
Logical deque order: Z, A, B, C
(read from head=15 → wraps at 0 → continues to tail-1=2)
AFTER pollFirst() — remove "Z":
result = elements[15] = "Z"
elements[15] = null ← clear slot (helps GC)
head = (15 + 1) & 15 = 0
head = 0, tail = 3
Logical deque order: A, B, C
BITWISE MASK: (index) & (capacity - 1)
Capacity is always a power of 2 (16, 32, 64...).
capacity - 1 is all-ones in binary (e.g., 15 = 0b1111).
Any index & mask naturally wraps around — faster than modulo.
Example: (15 + 1) & 15 = 16 & 15 = 0 (wraps to front)
(0 - 1) & 15 = -1 & 15 = 15 (wraps to back)
Resize — When and How
ArrayDeque resizes when the circular buffer becomes full — that is, when head == tail after an insertion. The resize copies elements in logical order (head to end, then 0 to tail-1) into a new array of double the capacity.
RESIZE SCENARIO (capacity 8, all 8 slots occupied):
Before resize:
elements: [ "E" | "F" | "G" | "H" | "A" | "B" | "C" | "D" ]
0 1 2 3 4 5 6 7
head = 4, tail = 4 (full — head == tail)
Logical order: A, B, C, D, E, F, G, H
(starting at head=4, wrapping around)
addLast("I") triggers resize:
1. Allocate new Object[16]
2. Copy elements from head (index 4) to end of old array → [A,B,C,D]
3. Copy elements from index 0 to tail-1 (index 3) → [E,F,G,H]
4. Result: new[0..7] = [A,B,C,D,E,F,G,H]
5. Insert "I" at new[8]
6. head = 0, tail = 9
After resize:
elements: [ "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | null×7 ]
0 1 2 3 4 5 6 7 8
head = 0, tail = 9
AMORTISED COST: each element is copied at most once per doubling.
Over n insertions, total copy work is 1 + 2 + 4 + ... + n = 2n.
Amortised per insertion: O(1).
CAPACITY IS ALWAYS A POWER OF 2:
This is enforced by the constructor. If you pass capacity 10,
ArrayDeque internally rounds up to 16. This makes the bitwise
mask trick always work correctly.
Memory Layout Advantage Over LinkedList
MEMORY COMPARISON (100 String elements, 64-bit JVM compressed refs):
ArrayDeque:
One Object[] array: 100 × 4 bytes = ~400 bytes for references
Array object header: 16 bytes
Total overhead: ~416 bytes
LinkedList:
100 × Node objects, each:
Object header: 16 bytes
item reference: 4 bytes
next reference: 4 bytes
prev reference: 4 bytes
Per-Node total: ~28 bytes × 100 = ~2800 bytes
ArrayDeque overhead: ~416 bytes
LinkedList overhead: ~2800 bytes (6.7× more)
CACHE LOCALITY:
ArrayDeque elements occupy consecutive memory addresses.
When the CPU loads elements[i], hardware prefetching loads
elements[i+1], [i+2], etc. into the L1 cache automatically.
LinkedList node references point to different heap locations.
Each node.next dereference causes a cache miss if the Node
was allocated at a different time and lives in a different
cache line. At high throughput this difference is measurable.
Core Operations with Examples
All Six Safe Methods — Both Ends
1// File: ArrayDequeOpsDemo.java
2
3import java.util.ArrayDeque;
4import java.util.Deque;
5
6public class ArrayDequeOpsDemo {
7
8 public static void main(String[] args) {
9
10 Deque<String> deque = new ArrayDeque<>();
11
12 // Build from both ends to show circular behaviour
13 deque.offerLast("Centre");
14 deque.offerFirst("Before-Centre");
15 deque.offerLast("After-Centre");
16 deque.offerFirst("Front");
17 deque.offerLast("Back");
18 System.out.println("After 5 inserts : " + deque);
19 System.out.println("Size : " + deque.size());
20
21 // Inspect both ends without removing
22 System.out.println("\n=== Peek at both ends ===");
23 System.out.println("peekFirst() : " + deque.peekFirst()); // Front
24 System.out.println("peekLast() : " + deque.peekLast()); // Back
25 System.out.println("Unchanged : " + deque);
26
27 // Remove from both ends
28 System.out.println("\n=== Poll from both ends ===");
29 System.out.println("pollFirst() : " + deque.pollFirst()); // Front
30 System.out.println("pollLast() : " + deque.pollLast()); // Back
31 System.out.println("After polls : " + deque);
32
33 // Safe methods on empty deque — null, no exception
34 System.out.println("\n=== Emptying and safe boundary checks ===");
35 deque.clear();
36 System.out.println("isEmpty() : " + deque.isEmpty());
37 System.out.println("pollFirst() : " + deque.pollFirst()); // null
38 System.out.println("peekLast() : " + deque.peekLast()); // null
39
40 // Throwing methods on empty deque
41 try {
42 deque.removeFirst();
43 } catch (java.util.NoSuchElementException e) {
44 System.out.println("removeFirst(): throws NoSuchElementException");
45 }
46
47 // contains() — O(n) linear scan
48 System.out.println("\n=== contains() — O(n) ===");
49 Deque<Integer> numbers = new ArrayDeque<>(java.util.List.of(10, 20, 30, 40, 50));
50 System.out.println("contains(30) : " + numbers.contains(30)); // true
51 System.out.println("contains(99) : " + numbers.contains(99)); // false
52 }
53}Output:
After 5 inserts : [Front, Before-Centre, Centre, After-Centre, Back]
Size : 5
=== Peek at both ends ===
peekFirst() : Front
peekLast() : Back
Unchanged : [Front, Before-Centre, Centre, After-Centre, Back]
=== Poll from both ends ===
pollFirst() : Front
pollLast() : Back
After polls : [Before-Centre, Centre, After-Centre]
=== Emptying and safe boundary checks ===
isEmpty() : true
pollFirst() : null
peekLast() : null
removeFirst(): throws NoSuchElementException
=== contains() — O(n) ===
contains(30) : true
contains(99) : false
ArrayDeque as a Stack — Replacing java.util.Stack
1// File: ArrayDequeStackDemo.java
2
3import java.util.ArrayDeque;
4import java.util.Deque;
5
6public class ArrayDequeStackDemo {
7
8 // Evaluate a simple postfix expression using ArrayDeque as a stack
9 static int evaluatePostfix(String expression) {
10 Deque<Integer> stack = new ArrayDeque<>();
11
12 for (String token : expression.trim().split("\\s+")) {
13 switch (token) {
14 case "+" -> { int b = stack.pop(); int a = stack.pop(); stack.push(a + b); }
15 case "-" -> { int b = stack.pop(); int a = stack.pop(); stack.push(a - b); }
16 case "*" -> { int b = stack.pop(); int a = stack.pop(); stack.push(a * b); }
17 case "/" -> { int b = stack.pop(); int a = stack.pop(); stack.push(a / b); }
18 default -> stack.push(Integer.parseInt(token));
19 }
20 }
21 return stack.pop(); // final result
22 }
23
24 public static void main(String[] args) {
25
26 // Stack operations — all operate on head (front)
27 Deque<String> callStack = new ArrayDeque<>();
28 System.out.println("=== ArrayDeque as LIFO stack ===");
29 callStack.push("main()");
30 callStack.push("processOrder()");
31 callStack.push("validatePayment()");
32 callStack.push("callPaymentGateway()");
33 System.out.println("Stack (top first) : " + callStack);
34 System.out.println("peek() : " + callStack.peek());
35
36 System.out.println("\nUnwinding call stack:");
37 while (!callStack.isEmpty()) {
38 System.out.println(" returning from: " + callStack.pop());
39 }
40
41 System.out.println();
42
43 // Postfix expression evaluation
44 System.out.println("=== Postfix expression evaluation ===");
45 String[] exprs = {
46 "3 4 +", // 3 + 4 = 7
47 "10 2 8 * + 3 -", // 10 + (2 * 8) - 3 = 23
48 "5 1 2 + 4 * + 3 -" // 5 + ((1+2)*4) - 3 = 14
49 };
50 for (String expr : exprs) {
51 System.out.printf(" %-25s = %d%n", expr, evaluatePostfix(expr));
52 }
53 }
54}Output:
=== ArrayDeque as LIFO stack ===
Stack (top first) : [callPaymentGateway(), validatePayment(), processOrder(), main()]
peek() : callPaymentGateway()
Unwinding call stack:
returning from: callPaymentGateway()
returning from: validatePayment()
returning from: processOrder()
returning from: main()
=== Postfix expression evaluation ===
3 4 + = 7
10 2 8 * + 3 - = 23
5 1 2 + 4 * + 3 - = 14
ArrayDeque as a FIFO Queue
1// File: ArrayDequeQueueDemo.java
2
3import java.util.ArrayDeque;
4import java.util.Queue;
5
6public class ArrayDequeQueueDemo {
7
8 record Request(String id, String endpoint, long timestampMs) {
9 @Override public String toString() {
10 return String.format("[%s] %s", id, endpoint);
11 }
12 }
13
14 public static void main(String[] args) {
15
16 // Declared as Queue — only FIFO methods are accessible
17 Queue<Request> requestQueue = new ArrayDeque<>();
18
19 System.out.println("=== ArrayDeque as FIFO queue ===");
20 requestQueue.offer(new Request("R001", "POST /api/orders", System.currentTimeMillis()));
21 requestQueue.offer(new Request("R002", "GET /api/products", System.currentTimeMillis()));
22 requestQueue.offer(new Request("R003", "PUT /api/cart/item", System.currentTimeMillis()));
23 requestQueue.offer(new Request("R004", "DELETE /api/session", System.currentTimeMillis()));
24
25 System.out.println("Queue size : " + requestQueue.size());
26 System.out.println("peek() (head) : " + requestQueue.peek()); // R001
27
28 System.out.println("\nProcessing in FIFO order:");
29 while (!requestQueue.isEmpty()) {
30 Request req = requestQueue.poll(); // always returns oldest
31 System.out.println(" Dispatched: " + req);
32 }
33
34 System.out.println("\nQueue empty: " + requestQueue.isEmpty());
35 System.out.println("poll() on empty: " + requestQueue.poll()); // null — safe
36 }
37}Output:
=== ArrayDeque as FIFO queue ===
Queue size : 4
peek() (head) : [R001] POST /api/orders
Processing in FIFO order:
Dispatched: [R001] POST /api/orders
Dispatched: [R002] GET /api/products
Dispatched: [R003] PUT /api/cart/item
Dispatched: [R004] DELETE /api/session
Queue empty: true
poll() on empty: null
Iterating ArrayDeque
1// File: ArrayDequeIterationDemo.java
2
3import java.util.ArrayDeque;
4import java.util.Deque;
5import java.util.Iterator;
6
7public class ArrayDequeIterationDemo {
8
9 public static void main(String[] args) {
10
11 Deque<String> pages = new ArrayDeque<>();
12 pages.offerLast("Home");
13 pages.offerLast("Products");
14 pages.offerLast("Cart");
15 pages.offerLast("Checkout");
16 pages.offerLast("Confirmation");
17
18 // for-each iterates head → tail (front to back — insertion order)
19 System.out.print("for-each (head→tail): ");
20 for (String page : pages) { System.out.print(page + " "); }
21 System.out.println();
22
23 // descendingIterator iterates tail → head (back to front)
24 System.out.print("descendingIterator : ");
25 Iterator<String> desc = pages.descendingIterator();
26 while (desc.hasNext()) { System.out.print(desc.next() + " "); }
27 System.out.println();
28
29 // Safe removal during iteration using iterator.remove()
30 Deque<Integer> scores = new ArrayDeque<>(java.util.List.of(88, 32, 75, 14, 91, 56, 44, 100));
31 System.out.println("\nBefore filter: " + scores);
32 Iterator<Integer> it = scores.iterator();
33 while (it.hasNext()) {
34 if (it.next() < 50) it.remove(); // safe — iterator manages modCount
35 }
36 System.out.println("After remove(<50): " + scores);
37
38 // removeIf — modern Java 8+ alternative
39 Deque<Integer> scores2 = new ArrayDeque<>(java.util.List.of(88, 32, 75, 14, 91, 56, 44, 100));
40 scores2.removeIf(score -> score < 50);
41 System.out.println("removeIf(<50) : " + scores2);
42 }
43}Output:
for-each (head→tail): Home Products Cart Checkout Confirmation
descendingIterator : Confirmation Checkout Cart Products Home
Before filter: [88, 32, 75, 14, 91, 56, 44, 100]
After remove(<50): [88, 75, 91, 56, 100]
removeIf(<50) : [88, 75, 91, 56, 100]
Real-World Example — PhonePe UPI Transaction History Buffer
A UPI payments app displays the last N transactions in the current session without hitting the database on every navigation. New transactions are appended to the tail. When the buffer is full, the oldest transaction is evicted from the head. The user can navigate forward (newest) or backward (oldest) through the window. ArrayDeque's O(1) both-end access handles all four operations.
1// File: TransactionEntry.java
2
3public record TransactionEntry(
4 String txnId,
5 String merchant,
6 double amount,
7 String status) {
8
9 @Override
10 public String toString() {
11 return String.format("[%s] %-18s Rs.%7.2f %s",
12 txnId, merchant, amount, status);
13 }
14}1// File: TransactionHistoryBuffer.java
2
3import java.util.ArrayDeque;
4import java.util.Deque;
5import java.util.Iterator;
6
7public class TransactionHistoryBuffer {
8
9 private final int bufferSize;
10 private final Deque<TransactionEntry> buffer;
11
12 public TransactionHistoryBuffer(int bufferSize) {
13 this.bufferSize = bufferSize;
14 this.buffer = new ArrayDeque<>(bufferSize + 1);
15 }
16
17 // Record a new transaction — evict oldest if buffer is full
18 public void record(TransactionEntry txn) {
19 if (buffer.size() == bufferSize) {
20 TransactionEntry evicted = buffer.pollFirst(); // remove oldest (head)
21 System.out.printf(" EVICTED : %s%n", evicted);
22 }
23 buffer.offerLast(txn); // append newest to tail
24 System.out.printf(" RECORDED : %s%n", txn);
25 }
26
27 // Show transactions in display order: newest first
28 public void displayNewestFirst() {
29 System.out.println("=".repeat(62));
30 System.out.printf(" TRANSACTION HISTORY (last %d)%n", buffer.size());
31 System.out.println("=".repeat(62));
32 Iterator<TransactionEntry> it = buffer.descendingIterator(); // tail→head
33 int rank = 1;
34 while (it.hasNext()) {
35 System.out.printf(" %2d. %s%n", rank++, it.next());
36 }
37 System.out.println("=".repeat(62));
38 }
39
40 // Show transactions in chronological order: oldest first
41 public void displayOldestFirst() {
42 System.out.println("=".repeat(62));
43 System.out.printf(" CHRONOLOGICAL HISTORY (last %d)%n", buffer.size());
44 System.out.println("=".repeat(62));
45 int rank = 1;
46 for (TransactionEntry txn : buffer) { // head→tail = oldest first
47 System.out.printf(" %2d. %s%n", rank++, txn);
48 }
49 System.out.println("=".repeat(62));
50 }
51
52 public TransactionEntry latestTransaction() { return buffer.peekLast(); }
53 public TransactionEntry oldestTransaction() { return buffer.peekFirst(); }
54
55 public static void main(String[] args) throws InterruptedException {
56
57 TransactionHistoryBuffer history = new TransactionHistoryBuffer(5);
58
59 System.out.println("--- Recording transactions ---");
60 history.record(new TransactionEntry("TXN-001","Zomato", 249.0, "SUCCESS"));
61 history.record(new TransactionEntry("TXN-002","BookMyShow", 599.0, "SUCCESS"));
62 history.record(new TransactionEntry("TXN-003","Jio Recharge", 239.0, "SUCCESS"));
63 history.record(new TransactionEntry("TXN-004","Amazon", 1299.0, "PENDING"));
64 history.record(new TransactionEntry("TXN-005","Swiggy", 189.0, "SUCCESS"));
65
66 System.out.println();
67 history.displayNewestFirst();
68
69 System.out.println("\n--- New transaction — buffer full, TXN-001 evicted ---");
70 history.record(new TransactionEntry("TXN-006","Flipkart", 2499.0, "SUCCESS"));
71
72 System.out.println();
73 history.displayNewestFirst();
74 System.out.println();
75 history.displayOldestFirst();
76
77 System.out.println("\n--- Quick access ---");
78 System.out.println("Latest : " + history.latestTransaction());
79 System.out.println("Oldest : " + history.oldestTransaction());
80 }
81}Output:
--- Recording transactions ---
RECORDED : [TXN-001] Zomato Rs. 249.00 SUCCESS
RECORDED : [TXN-002] BookMyShow Rs. 599.00 SUCCESS
RECORDED : [TXN-003] Jio Recharge Rs. 239.00 SUCCESS
RECORDED : [TXN-004] Amazon Rs. 1299.00 PENDING
RECORDED : [TXN-005] Swiggy Rs. 189.00 SUCCESS
==============================================================
TRANSACTION HISTORY (last 5)
==============================================================
1. [TXN-005] Swiggy Rs. 189.00 SUCCESS
2. [TXN-004] Amazon Rs. 1299.00 PENDING
3. [TXN-003] Jio Recharge Rs. 239.00 SUCCESS
4. [TXN-002] BookMyShow Rs. 599.00 SUCCESS
5. [TXN-001] Zomato Rs. 249.00 SUCCESS
==============================================================
--- New transaction — buffer full, TXN-001 evicted ---
EVICTED : [TXN-001] Zomato Rs. 249.00 SUCCESS
RECORDED : [TXN-006] Flipkart Rs. 2499.00 SUCCESS
==============================================================
TRANSACTION HISTORY (last 5)
==============================================================
1. [TXN-006] Flipkart Rs. 2499.00 SUCCESS
2. [TXN-005] Swiggy Rs. 189.00 SUCCESS
3. [TXN-004] Amazon Rs. 1299.00 PENDING
4. [TXN-003] Jio Recharge Rs. 239.00 SUCCESS
5. [TXN-002] BookMyShow Rs. 599.00 SUCCESS
==============================================================
==============================================================
CHRONOLOGICAL HISTORY (last 5)
==============================================================
1. [TXN-002] BookMyShow Rs. 599.00 SUCCESS
2. [TXN-003] Jio Recharge Rs. 239.00 SUCCESS
3. [TXN-004] Amazon Rs. 1299.00 PENDING
4. [TXN-005] Swiggy Rs. 189.00 SUCCESS
5. [TXN-006] Flipkart Rs. 2499.00 SUCCESS
==============================================================
--- Quick access ---
Latest : [TXN-006] Flipkart Rs. 2499.00 SUCCESS
Oldest : [TXN-002] BookMyShow Rs. 599.00 SUCCESS
Performance Considerations
| Operation | ArrayDeque | LinkedList | java.util.Stack |
|---|---|---|---|
| addFirst / push | O(1) amortised | O(1) | O(1) + lock |
| addLast / offer | O(1) amortised | O(1) | O(1) + lock |
| removeFirst / pop | O(1) amortised | O(1) | O(1) + lock |
| removeLast | O(1) amortised | O(1) | O(n) + lock |
| peekFirst / peek | O(1) | O(1) | O(1) + lock |
| peekLast | O(1) | O(1) | N/A |
| contains(e) | O(n) | O(n) | O(n) + lock |
| Resize / growth | O(n) amortised | N/A | O(n) amortised |
| Memory per element | ~4-8 bytes | ~28 bytes (Node) | ~4 bytes + overhead |
| Iteration speed | Fast (cache-friendly) | Slower (pointer-chased) | Moderate |
Stack.removeLast is O(n): java.util.Stack inherits Vector's structure — there is no tail pointer. Removing the last element requires traversal. ArrayDeque.removeLast() is O(1) via the tail index. This is a genuine asymptotic difference, not just a constant-factor improvement.
Resize is O(n) amortised: Each element is copied at most once per doubling cycle. Over n insertions, total copy work across all resizes is bounded by 2n. The amortised cost per insertion is O(1). This is the same guarantee that makes ArrayList append O(1) amortised.
Thread safety: ArrayDeque is not thread-safe. For concurrent producer-consumer with both-ends access, use LinkedBlockingDeque from java.util.concurrent. For single-producer single-consumer FIFO, ArrayBlockingQueue is more memory-efficient.
Best Practices
Declare the variable as Deque<E> for both-ends use, Queue<E> for FIFO-only use. Deque<String> history = new ArrayDeque<>() exposes the full both-ends API. Queue<String> tasks = new ArrayDeque<>() restricts callers to FIFO semantics only, which prevents accidental use of addFirst() when the intent is FIFO. Choose the narrowest interface that matches the actual access pattern.
Pre-size ArrayDeque when the expected number of elements is known. new ArrayDeque<>(expectedSize) allocates a backing array large enough to hold expectedSize elements without triggering any resize. The internal capacity is rounded up to the next power of 2. This eliminates O(n) resize operations and the temporary double-allocation during growth for bulk-loaded deques.
Use push()/pop()/peek() for stack semantics, offer()/poll() for queue semantics. The naming signals intent to readers. A method that uses push and pop reads as a stack. A method that uses offer and poll reads as a queue. Mixing them in one method (e.g., push to enqueue and pollLast to dequeue from the wrong end) produces correct but confusing code that is hard to maintain.
Clear slots after removal — ArrayDeque already does this for you. When pollFirst() or pollLast() removes an element, ArrayDeque nulls the vacated array slot. This is essential for garbage collection — if the slot were not cleared, the Object[] would hold a strong reference to the removed element even though logically it has been removed. This is handled internally and is one reason ArrayDeque is preferred over manually managed circular arrays.
Common Mistakes
Mistake 1 — Inserting null Into ArrayDeque
1Deque<String> events = new ArrayDeque<>();
2events.offerLast("LoginEvent");
3
4// WRONG — ArrayDeque.offerFirst(null) throws NullPointerException
5// events.offerFirst(null); // NullPointerException
6
7// Why null is prohibited: pollFirst() returns null to signal empty deque.
8// If null elements were stored, the caller cannot distinguish between
9// "got a null element" and "deque was empty — no element returned".
10
11// CORRECT — use a non-null sentinel value or Optional<E> as element type
12events.offerFirst("SHUTDOWN"); // sentinel for consumer shutdown signal
13java.util.Optional<String> opt = java.util.Optional.empty();
14// OR declare: Deque<Optional<String>> for nullable business valuesMistake 2 — Using java.util.Stack in New Code
1// WRONG — every push/pop is synchronised, even on a single thread
2java.util.Stack<String> frames = new java.util.Stack<>();
3frames.push("frameA"); // acquires mutex
4frames.push("frameB"); // acquires mutex
5String top = frames.pop(); // acquires mutex — pure overhead in single-threaded code
6
7// WRONG — Stack exposes Vector methods that make no sense for a stack
8frames.elementAt(0); // random access violates stack encapsulation
9frames.insertElementAt("frameX", 1); // breaks LIFO ordering
10
11// CORRECT — ArrayDeque as stack — zero synchronisation, correct encapsulation
12Deque<String> frames2 = new ArrayDeque<>();
13frames2.push("frameA");
14frames2.push("frameB");
15String top2 = frames2.pop(); // O(1) amortised, no lockMistake 3 — Confusing Iteration Order With Priority Order
1// ArrayDeque iterates in INSERTION order (head → tail), NOT value order
2Deque<Integer> nums = new ArrayDeque<>();
3nums.offerLast(30); nums.offerLast(10); nums.offerLast(50); nums.offerLast(20);
4
5System.out.println("for-each: " + nums); // [30, 10, 50, 20] — insertion order
6// NOT [10, 20, 30, 50] — ArrayDeque is not a sorted structure
7
8// If processing in value order is needed, use PriorityQueue
9java.util.Queue<Integer> prioritised = new java.util.PriorityQueue<>();
10prioritised.offer(30); prioritised.offer(10); prioritised.offer(50); prioritised.offer(20);
11System.out.print("PriorityQueue poll: ");
12while (!prioritised.isEmpty()) System.out.print(prioritised.poll() + " "); // 10 20 30 50
13System.out.println();Output:
for-each: [30, 10, 50, 20]
PriorityQueue poll: 10 20 30 50
Mistake 4 — Using LinkedList When ArrayDeque Handles the Use Case
1// WRONG for pure stack/queue/deque use — allocates a Node per element
2java.util.LinkedList<String> history = new java.util.LinkedList<>();
3history.addFirst("Page1");
4history.addLast("Page2");
5
6// CORRECT — ArrayDeque for all push/pop/offer/poll/addFirst/addLast use
7Deque<String> history2 = new ArrayDeque<>();
8history2.addFirst("Page1");
9history2.addLast("Page2");
10
11// LinkedList as Deque earns its place ONLY when List methods are also needed
12// on the same variable: get(index), set(index, e), subList(from, to)
13// For pure head/tail operations, ArrayDeque is always faster and lighter.Interview Questions
Q1. What is ArrayDeque in Java and what are its main advantages over LinkedList and Stack?
ArrayDeque<E> is a concrete Deque implementation backed by a resizable circular array. Every operation at both ends is O(1) amortised. Compared to LinkedList: no per-element Node allocation means less GC pressure, and the contiguous array layout gives better CPU cache performance. Compared to java.util.Stack: Stack extends Vector, which synchronises every push(), pop(), and peek() call even in single-threaded code — pure overhead. ArrayDeque has no synchronisation. The Java documentation explicitly recommends ArrayDeque as the preferred replacement for both.
Q2. How does ArrayDeque implement O(1) operations at both ends?
ArrayDeque maintains two integer indices — head (the front element) and tail (the next insertion slot at the back) — into a circular Object[] array. addFirst(e) decrements head using bitwise wrap: head = (head - 1) & (capacity - 1), then writes elements[head] = e. addLast(e) writes elements[tail] = e, then increments tail. pollFirst() reads elements[head], nulls the slot, and increments head. All are direct array index operations — O(1). Resize happens when the array is full: a new array of double capacity is allocated, elements are copied in logical order, and head is reset to 0. This is O(n) but amortised O(1) over n insertions.
Q3. Why is the capacity of ArrayDeque always a power of 2?
The circular wrap-around uses bitwise AND masking: nextIndex = (index + 1) & (capacity - 1). This only produces correct wrap-around results when capacity - 1 is all-ones in binary — which happens precisely when capacity is a power of 2. For example, capacity 16 gives mask 15 (binary 1111). (15 + 1) & 15 = 16 & 15 = 0 — correctly wraps to the start. Bitwise AND is faster than modulo division, which is why Java enforces power-of-2 capacities internally. If you pass capacity 10 to the constructor, ArrayDeque rounds up to 16.
Q4. What is the difference between the throwing and null-returning method groups in ArrayDeque?
Every operation exists in two forms. The throwing group — addFirst, removeFirst, getFirst (and equivalents for last) — throws NoSuchElementException when the deque is empty, or IllegalStateException when capacity is exceeded (for bounded implementations). The null-returning group — offerFirst, pollFirst, peekFirst — returns null or false instead. For ArrayDeque, which is unbounded, capacity overflow never occurs, so the only practical difference is empty-deque behaviour. Production code should use the null-returning group for routine processing loops and reserve the throwing group for situations where an empty deque is a genuine programming error.
Q5. Can ArrayDeque be used as both a stack and a queue on the same object?
Yes — this is one of ArrayDeque's most useful properties. Both access patterns use opposite ends of the same circular buffer. Stack operations (push/pop) operate on the head. Queue operations (offer/poll) add to the tail and remove from the head. A sliding window buffer uses offerLast for new arrivals and pollFirst for eviction. All three patterns can coexist in the same ArrayDeque object without conflict, as long as the caller is consistent about which end represents the "front" for each semantic.
Q6. Why does ArrayDeque prohibit null elements?
pollFirst() and pollLast() return null to signal that the deque is empty — there is no separate isEmpty() check required before polling. If null elements were stored, a null return from poll would be ambiguous: it could mean "the deque was empty" or "the front element was null." Prohibiting null removes that ambiguity entirely. LinkedList allows null elements because it uses size > 0 as its emptiness signal rather than relying on the returned value. If a null-value sentinel is genuinely needed, use Optional<E> as the element type.
FAQs
What is the initial capacity of ArrayDeque and how does it grow?
The default internal capacity is 16 elements. The constructor new ArrayDeque<>(n) pre-allocates an array large enough for n elements, rounded up to the next power of 2. When the array becomes full, ArrayDeque allocates a new array of approximately double the capacity, copies all elements in logical order (head to tail), and resets head to 0. Growth is always to the next power of 2 to maintain the bitwise-mask wrap-around property.
Is ArrayDeque the same as a circular buffer?
Functionally yes. A circular buffer is any fixed-size buffer where reads and writes wrap around from the end back to the beginning. ArrayDeque uses exactly this structure with resizing on top. The key practical difference: a true fixed-size circular buffer has a maximum capacity and overwrites or rejects new elements when full. ArrayDeque never rejects elements — it resizes instead. For a true fixed-capacity circular buffer that overwrites old data, you would need to implement the eviction policy manually using ArrayDeque with a size guard, as shown in the real-world example above.
When should I use ArrayDeque over PriorityQueue?
Use ArrayDeque when processing order is either FIFO (arrival order) or LIFO (most-recently-added first). Use PriorityQueue when processing order depends on element value or priority — the smallest (or custom-highest-priority) element should always be processed next. ArrayDeque provides O(1) both-ends access; PriorityQueue provides O(log n) poll() and O(1) peek(). If your task queue needs priority-based dispatch, PriorityQueue is the right choice. If tasks should be processed in the order they arrived, ArrayDeque is faster.
Can ArrayDeque be iterated safely while removing elements?
Removing elements during for-each iteration throws ConcurrentModificationException — same as ArrayList and HashSet. Use iterator().remove() or removeIf(predicate) for safe removal during traversal. Both maintain modCount consistency internally. removeIf() is the cleaner Java 8+ choice for predicate-based removal and handles the bookkeeping transparently.
How does ArrayDeque compare to ArrayBlockingQueue for queue use?
Both use array-backed circular buffers, but for different scenarios. ArrayDeque is single-threaded, unbounded (grows as needed), and O(1) amortised. ArrayBlockingQueue is thread-safe, bounded (fixed capacity at construction), and uses a ReentrantLock for thread safety. ArrayBlockingQueue.put() blocks when full, take() blocks when empty — ideal for bounded producer-consumer pipelines. ArrayDeque is the choice for single-threaded workloads; ArrayBlockingQueue for concurrent ones.
Does ArrayDeque support random access by index?
No. ArrayDeque implements Deque not List, so there is no get(int index) method. It does not expose the internal array index — only the head and tail operations are public. If index-based access alongside deque operations is needed, LinkedList is the only standard Deque implementation that also implements List. For most cases where you think you need index access on a deque, you likely need either an ArrayList or a different design.
Summary
ArrayDeque<E> is the practical workhorse behind most Java stack and queue patterns. Its circular resizable array gives O(1) amortised operations at both ends with no per-element allocation overhead, no synchronisation cost, and cache-friendly sequential memory. The Java documentation recommends it explicitly over java.util.Stack and java.util.LinkedList for all non-concurrent queue and stack use cases.
Two implementation details that matter for interviews: capacity is always a power of 2 so the bitwise-AND wrap-around works correctly, and null elements are prohibited because poll() uses null as the "empty deque" signal. Both are design decisions made deliberately for performance and correctness.
Declare the variable as Deque<E> for both-ends code, Queue<E> for FIFO-only code. Pre-size when the expected element count is known. Never insert null. And when you review a PR that uses new Stack<>(), you now have the exact technical argument for why new ArrayDeque<>() is the right replacement.
What to Read Next
| Topic | Link |
|---|---|
| How the Deque interface defines all 12 method pairs that ArrayDeque implements | Java Collections Framework → |
| How LinkedList implements Deque differently and when it is the right choice | Java LinkedList → |
| How ArrayList's resizable array compares to ArrayDeque's circular buffer design | Java ArrayList → |
| How the Iterator interface enables safe traversal and removal on ArrayDeque | Java Iterator → |
| How Java Streams work with ArrayDeque elements for filter and collect operations | Java Streams API → |