AlgoMaster Logo

ArrayDeque

High Priority12 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

ArrayDeque is a resizable, circular-array implementation of the Deque interface. Use it when you need a queue or a stack today, because it's faster than LinkedList for queue operations and faster than Stack for stack operations. This lesson covers how the circular buffer works internally, why it beats the older alternatives, the rules around nulls and capacity, and a few realistic e-commerce use cases (a job queue for processing orders, a sliding window of recent events, and a modern replacement for the legacy Stack-based undo).

What ArrayDeque Actually Is

Internally, ArrayDeque is one fixed-size array plus two integer indices, head and tail. When you push at the front, head moves backward. When you offer at the tail, tail moves forward. When either index runs off the end of the array, it wraps around to the other side. The array isn't shifted, and nothing gets copied on a normal push or poll. That's what "circular" means here: the indices wrap, the data stays put.

The capacity is always a power of two. The default initial capacity is 16. When the array fills up, ArrayDeque allocates a new array of double the size and copies the existing elements into it, head-first. That's the only time data moves.

Here's the smallest possible example, using it as a FIFO queue for orders waiting to be processed by a fulfillment worker:

offer appends at the tail; poll removes from the head. Three orders go in, two come out in arrival order, one remains. No shifting, no copying, just two index updates per operation.

offer, poll, push, pop, peek, and peekLast are all O(1) amortized. The only operation that isn't is the rare grow when the array fills up, and that one is O(n).

Why ArrayDeque Beats LinkedList

Both ArrayDeque and LinkedList implement Deque, so they have the same API for queue and stack work. The difference is in what's happening internally. LinkedList allocates a small node object for every element you add, with a reference to the previous and next nodes. ArrayDeque writes the element straight into a slot in its backing array.

That difference matters for two reasons.

The first is allocation. Every LinkedList.add creates a new Node object on the heap. A million-element LinkedList is a million-and-one heap objects, plus the references between them. An ArrayDeque with a million elements is one array.

The second is cache locality. When the CPU loads an ArrayDeque element, the next few elements are usually already in the same cache line, because they're next to each other in memory. LinkedList nodes can sit anywhere in the heap, so iterating one tends to miss the cache more often.

Your exact numbers will vary by machine and JVM, but the ratio is the part that matters. ArrayDeque is consistently faster for queue work because it allocates less and stays cache-friendly. The Javadoc itself says it's "likely to be faster than LinkedList when used as a queue", and the same applies for stacks.

LinkedList still has one thing going for it: O(1) removal of an interior node when you already hold a reference to it (via the ListIterator.remove path). If you're not doing that, there's no reason to prefer it for queue or stack work.

Why ArrayDeque Beats Stack

java.util.Stack is a class that's been around since Java 1.0. It extends Vector, which means every operation goes through Vector's synchronized methods, even when only one thread is using it. Every push and pop takes a lock you don't need.

ArrayDeque isn't thread-safe. If you're using a stack from one thread, you don't pay for synchronization. If you actually need a thread-safe stack, use a ConcurrentLinkedDeque or wrap the structure yourself, rather than Stack.

The Javadoc for ArrayDeque is direct about this: "This class is likely to be faster than Stack when used as a stack". That's the official Java recommendation: use ArrayDeque for stack work. The legacy Stack class sticks around for backward compatibility.

Here's an undo stack for a shopping cart, written the modern way. Every user action (add item, remove item, apply coupon) gets pushed; pressing undo pops the most recent one off.

push adds at the head, pop removes from the head, peek looks at the head without removing. That's a textbook LIFO stack, with no synchronization overhead and one backing array instead of a chain of nodes.

If you're starting a new codebase today, the rule is simple: use ArrayDeque for stacks and queues. Reserve Stack for the rare case where you're maintaining legacy code that expects it.

How the Circular Buffer Wraps

The "circular" in circular buffer means the array's slots are reused as the indices wrap around the end, so the data isn't shifted when you remove from the front.

Consider an ArrayDeque with capacity 8 (a power of two, like all ArrayDeque capacities). It starts empty, with both head and tail at slot 0. After a few offer calls and a few poll calls, the active region of the array can sit anywhere, possibly even wrapping around the end.

head points to the first real element; tail points to the next empty slot. offer writes at tail and advances tail. poll reads at head, clears the slot, and advances head. Each "advance" uses an & (capacity - 1) mask, which works because the capacity is a power of two. That mask is what makes the indices wrap from slot 7 back to slot 0 without a conditional.

The same buffer after a few more operations push tail past the end:

The active region runs from slot 6, through slot 7, then wraps to slot 0 and slot 1. Logically the queue contains four orders in order: 105, 106, 107, 108. Physically they're split across the array, but iteration walks head forward with the wrapping mask, so the consumer never has to know.

When tail catches up to head (the array is full), the next offer triggers a resize. A new array of double the capacity is allocated, the elements are copied so head lands at slot 0, and the indices are reset. That copy is the only O(n) operation in ArrayDeque's normal lifecycle, and it happens at most log2(n) times for a buffer that grows to size n.

Nulls Are Not Allowed

ArrayDeque rejects null elements. Trying to add one throws NullPointerException on the spot.

There's a real reason for the restriction. Methods like peek and poll return null to mean "the deque is empty". If null were a valid element, the caller couldn't tell whether poll() returned a real element or signalled emptiness. The class avoids the ambiguity by banning null entirely.

If you need to represent "no value" in a queue, wrap the elements in something that can carry that meaning, like Optional<T>, or use a sentinel value:

Now "no coupon" is a real value of type Optional.empty() instead of null, and ArrayDeque is happy to store it.

Capacity, Growth, and Pre-Sizing

ArrayDeque has three constructors:

ConstructorWhat it does
new ArrayDeque<>()Allocates capacity 16.
new ArrayDeque<>(int numElements)Allocates the smallest power of two >= numElements. Useful if you already know the rough size.
new ArrayDeque<>(Collection<? extends E> c)Allocates enough capacity to hold c's elements, then copies them in.

When the array fills, the deque doubles it. There's no shrink. If you push a million orders into an ArrayDeque and then poll all of them out, the backing array stays at roughly a million slots until the deque is garbage collected. For long-lived deques where the size fluctuates wildly, that's something to be aware of; replacing the deque with a fresh one is the simplest way to reclaim the space if you need to.

Pre-sizing matters when you know the size in advance. If a daily batch job processes 50,000 orders, starting with new ArrayDeque<>(50_000) avoids about 12 doublings and the corresponding copies:

A grow allocates a new array and copies every element. Each individual offer is O(1) amortized, but the specific call that triggers a grow is O(n). Pre-sizing flattens that bump.

A Sliding Window of Recent Events

ArrayDeque is well-suited to a bounded buffer of the last N items. The pattern: offer at the tail on every event, and if the size exceeds the limit, poll from the head. The deque always holds the last N events, in arrival order.

A storefront wants to keep the last 5 product views per session so it can show "recently viewed" on the homepage. Older views drop off automatically.

The first two views, "Notebook" and "Pen", aged out as new ones arrived. The deque is iterated head-to-tail, so the oldest survivor comes first. Both the poll and the offer are O(1), so the whole pattern stays cheap even with millions of events.

A LinkedList would do the same thing, but with one heap node allocated per event. For a high-traffic feed, the difference adds up.

A Work-Stealing Job Queue

Consider a fulfillment system with a single producer (the order intake API) and a single consumer (a worker thread that processes orders). ArrayDeque is a good fit when there's no contention, because there's no synchronization overhead. If you need concurrency, switch to ConcurrentLinkedDeque or LinkedBlockingDeque; ArrayDeque is for single-threaded or externally-synchronized use.

For a simulation that runs on the main thread, this pattern works directly:

Normal orders arrive at the tail via offer. An urgent order jumps the line via push, which adds at the head. The worker always takes from the head with poll, so the urgent order is processed first, then the others in arrival order. The fact that ArrayDeque supports both ends in O(1) is what makes this priority-injection trick cheap.

If two threads were touching jobs at the same time, this would be wrong: ArrayDeque isn't thread-safe. For a concurrent variant of the same idea, the equivalent type is java.util.concurrent.LinkedBlockingDeque, which keeps the two-ended API but adds proper locking.

When to Use ArrayDeque

Three cases come up a lot:

Use caseWhy ArrayDeque fits
Plain FIFO queue (BFS, job queue, message buffer)Faster than LinkedList, no synchronization overhead.
LIFO stack (undo history, expression evaluation, recursion-to-iteration)Faster than Stack, no synchronization overhead, no legacy baggage.
Sliding-window buffer of the last N eventsO(1) at both ends makes the eviction-on-overflow pattern trivial.

When not to use it: when you need concurrent access (use a concurrent deque), when you need to insert in the middle (use an ArrayList or a different structure entirely), when you need O(1) lookup by value (use a HashSet or HashMap), or when you need null elements (use a LinkedList, though see if you can model the absence differently first).

Quiz

ArrayDeque Quiz

10 quizzes