ArrayDeque
ArrayDeque is the recommended general-purpose Deque implementation in Java. It is backed by a resizable circular array that provides O(1) amortised time for all head and tail operations — addFirst, addLast, pollFirst, pollLast, peekFirst, peekLast. ArrayDeque is faster than LinkedList for deque and queue operations because it avoids per-element node allocation and achieves better cache locality. It is faster than the legacy Stack class because it has no synchronisation overhead. The circular array design allows both ends to advance efficiently without shifting elements. This entry covers the circular array internals, performance characteristics, the resizing strategy, practical usage as both stack and queue, and the scenarios where ArrayDeque is the correct default choice.
Circular Array Internals
// ── Circular array visualisation ──────────────────────────────────────
//
// Circular array with capacity=8, initially empty:
// [ _, _, _, _, _, _, _, _ ]
// ^head=0 ^tail=0 (empty: head==tail)
//
// addLast("A"):
// [ A, _, _, _, _, _, _, _ ]
// ^head=0 tail=1^
//
// addLast("B"), addLast("C"):
// [ A, B, C, _, _, _, _, _ ]
// ^head=0 tail=3^
//
// addFirst("Z"): head wraps to index 7
// [ A, B, C, _, _, _, _, Z ]
// tail=3^ ^head=7 (decremented with wraparound)
//
// pollFirst(): removes Z (head index 7), head wraps back to 0
// [ A, B, C, _, _, _, _, _ ]
// ^head=0 tail=3^
//
// Key insight: no element shifting — only pointer arithmetic
// ── ArrayDeque construction ───────────────────────────────────────────
// Default initial capacity: 16 (Java 11+, was 8 in earlier versions)
ArrayDeque<String> deque1 = new ArrayDeque<>();
// With initial capacity hint (avoids resizing for known sizes):
ArrayDeque<String> deque2 = new ArrayDeque<>(100);
// Constructed from collection (adds in iteration order):
ArrayDeque<String> deque3 = new ArrayDeque<>(List.of("a", "b", "c"));
// ── Internal representation — capacity always power of 2 ──────────────
// The implementation ensures capacity is always 2^n
// This enables fast modulo: index = pointer & (capacity - 1)
// Instead of: index = pointer % capacity (slower division)
//
// Capacity progression: 16 → 32 → 64 → 128 → ...
// Triggered by: addFirst or addLast when (tail == head) after increment
// ── Wraparound arithmetic ──────────────────────────────────────────────
// For a deque with capacity=8:
// head at index 1, tail at index 4: [_, A, B, C, _, _, _, _]
//
// addFirst("Z"): head = (1 - 1) & 7 = 0 → [Z, A, B, C, _, _, _, _]
// addFirst("Y"): head = (0 - 1) & 7 = 7 → [Z, A, B, C, _, _, _, Y] (wraps!)
//
// pollFirst(): removes index 7 (Y), head = (7 + 1) & 7 = 0
// Result: head=0, deque=[Z, A, B, C]Performance — Comparison with LinkedList and Stack
// ── Performance benchmark context ────────────────────────────────────
// Typical relative throughput for 10 million operations (single-threaded):
//
// Operation ArrayDeque LinkedList Stack (Vector)
// ──────────────────────────────────────────────────────────────
// offer/addLast ~3.5B ops/s ~1.2B ops/s ~0.3B ops/s
// poll/removeFirst ~3.0B ops/s ~1.0B ops/s ~0.3B ops/s
// push (addFirst) ~3.5B ops/s ~1.2B ops/s ~0.3B ops/s
// pop (removeFirst) ~3.0B ops/s ~1.0B ops/s ~0.3B ops/s
//
// ArrayDeque is ~3x faster than LinkedList and ~10x faster than Stack
// for typical stack/queue operations
// ── Memory usage comparison ───────────────────────────────────────────
// For 1 million String elements:
//
// ArrayDeque:
// Array of 2^21 = 2,097,152 reference slots × 8 bytes = 16MB
// + ArrayDeque object header (~32 bytes)
// = ~16MB total (low overhead)
//
// LinkedList:
// 1,000,000 Node objects × ~48 bytes each = ~48MB
// (each Node: 16B header + 8B prev + 8B next + 8B element = 40B + padding)
// = ~3x more memory than ArrayDeque
// ── When to use each ─────────────────────────────────────────────────
// ArrayDeque:
// ✓ Stack operations (push/pop from front)
// ✓ Queue operations (offer tail, poll head)
// ✓ Deque operations (both ends)
// ✓ Single-threaded use
// ✓ Memory efficiency matters
// ✓ Default choice in almost all cases
//
// LinkedList:
// ✓ Frequent removal from the MIDDLE during iteration
// ✓ Already using as List AND queue (both interfaces needed)
// ✗ Never preferred over ArrayDeque for pure queue/stack use
//
// Stack (legacy):
// ✗ Never use in new code — use ArrayDeque instead
//
// Collections.synchronizedDeque(new ArrayDeque<>()):
// ✓ Simple thread safety needed, low concurrency
//
// LinkedBlockingDeque:
// ✓ Multi-threaded access, blocking semantics needed
// ✓ Bounded capacity for backpressureCommon Patterns and Idioms
// ── ArrayDeque as a stack — recommended pattern ─────────────────────
// Declare as Deque<T> — programs to interface, not implementation
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // addFirst(1) — [1]
stack.push(2); // addFirst(2) — [2, 1]
stack.push(3); // addFirst(3) — [3, 2, 1]
System.out.println(stack.peek()); // 3 — top of stack, no removal
System.out.println(stack.pop()); // 3 — removed
System.out.println(stack.pop()); // 2 — removed
// Safe pop with isEmpty check:
while (!stack.isEmpty()) {
System.out.println(stack.pop());
}
// ── ArrayDeque as a queue — recommended pattern ──────────────────────
// Declare as Queue<T> if only queue operations are needed
Queue<String> queue = new ArrayDeque<>();
queue.offer("first"); // addLast — null-safe, returns false if full
queue.offer("second");
queue.offer("third");
System.out.println(queue.peek()); // "first" — no removal
System.out.println(queue.poll()); // "first" — removed
System.out.println(queue.size()); // 2
// Process all items:
while (!queue.isEmpty()) {
process(queue.poll());
}
// ── Null is prohibited ────────────────────────────────────────────────
ArrayDeque<String> deque = new ArrayDeque<>();
// deque.offer(null); // NullPointerException — prohibited
// deque.push(null); // NullPointerException — prohibited
// deque.addFirst(null); // NullPointerException — prohibited
// This is SAFER than LinkedList which accepts null:
String result = deque.poll(); // null means EMPTY — unambiguous
// ── Full Deque usage — both ends ──────────────────────────────────────
Deque<String> workList = new ArrayDeque<>();
// Urgent items go to front, normal items go to back
workList.offerLast("normal-task-1");
workList.offerLast("normal-task-2");
workList.offerFirst("urgent-task"); // jumps to front
System.out.println(workList.pollFirst()); // "urgent-task"
System.out.println(workList.pollFirst()); // "normal-task-1"
System.out.println(workList.pollFirst()); // "normal-task-2"
// ── Iterating — in logical order (head to tail) ───────────────────────
Deque<Integer> nums = new ArrayDeque<>(List.of(1, 2, 3, 4, 5));
// Forward (head to tail):
for (int n : nums) {
System.out.print(n + " "); // 1 2 3 4 5
}
// Reverse (tail to head) using descending iterator:
Iterator<Integer> reverse = nums.descendingIterator();
while (reverse.hasNext()) {
System.out.print(reverse.next() + " "); // 5 4 3 2 1
}Resizing and Capacity Management
// ── Capacity hint for known size ─────────────────────────────────────
// If processing exactly 10,000 tasks:
Deque<Task> taskQueue = new ArrayDeque<>(10_000);
// Avoids multiple resize operations during the initial fill
// If size unknown but expected large:
Deque<LogEntry> logQueue = new ArrayDeque<>(4096); // good starting size
// ── Resizing behaviour ────────────────────────────────────────────────
// new ArrayDeque() → initial capacity 16 (Java 11+)
// After 16 adds: → resize to 32
// After 32 adds: → resize to 64
// After 1024 adds: → resize to 2048
// etc.
//
// Total copy cost for n adds = 16 + 32 + 64 + ... + n = O(n)
// Amortised cost per add = O(n)/n = O(1)
// ── No shrinking — memory impact ──────────────────────────────────────
Deque<byte[]> largeQueue = new ArrayDeque<>();
// Fill with 100,000 large objects:
for (int i = 0; i < 100_000; i++) {
largeQueue.offer(new byte[1024]); // queue grows to capacity ~131,072
}
// Drain all elements:
while (!largeQueue.isEmpty()) {
largeQueue.poll(); // elements removed, but array stays at ~131,072 slots
}
// Memory for ~131,072 reference slots is still allocated!
// (~1MB of reference array, not counting the byte[] objects themselves)
// If persistent large memory use is a problem, replace with fresh deque:
largeQueue = new ArrayDeque<>(); // old deque eligible for GC, fresh one allocated
// ── Checking current capacity (not directly available) ────────────────
// ArrayDeque does not expose capacity() — only size()
// To estimate: after filling, capacity >= next power of 2 above size
Deque<Integer> d = new ArrayDeque<>();
for (int i = 0; i < 100; i++) d.offer(i);
System.out.println(d.size()); // 100
// Internal capacity is 128 (next power of 2 above 100)
// ── Thread safety — ArrayDeque is NOT thread-safe ─────────────────────
// For concurrent use, do NOT synchronise ArrayDeque externally in general —
// use purpose-built concurrent implementations:
// Option 1: ConcurrentLinkedDeque — non-blocking, high throughput
Deque<Task> concurrent1 = new ConcurrentLinkedDeque<>();
// Option 2: LinkedBlockingDeque — blocking, optionally bounded
BlockingDeque<Task> concurrent2 = new LinkedBlockingDeque<>(1000);
// Option 3: Synchronised wrapper (low concurrency only)
Deque<Task> concurrent3 =
(Deque<Task>) Collections.synchronizedCollection(new ArrayDeque<>());
// Note: iteration still requires external synchronisation