1. Introduction to ArrayDeque

ArrayDeque is a resizable-array implementation of Java's Deque interface. The name combines “array” and “deque,” or double-ended queue. It allows elements to be inserted and removed from both the front and the back.

The main advantage of ArrayDeque is that it can serve as either a queue or a stack. For a FIFO queue, a program can add elements at the tail and remove them from the head. For a LIFO stack, it can push and pop elements at the head. It is commonly preferred over LinkedList for ordinary queue operations and over the legacy Stack class for ordinary stack operations.

The underlying array is circular: when an index reaches the end of the array, it wraps around to the beginning. This allows front and back operations without shifting every element after each removal.

Common uses include:

  • A FIFO queue for pending jobs or orders.
  • A stack for undo history or expression processing.
  • A rolling buffer of recent events.
  • A double-ended sequence where urgent work may be inserted at the front.
  • Breadth-first search and other algorithms that need an efficient queue.

ArrayDeque is not a sorted collection and does not automatically prioritize items. The program controls the order through the methods it calls.

2. ArrayDeque in the Java Collections Framework

ArrayDeque implements Deque, which extends Queue, which in turn extends Collection. As a result, it supports the general collection methods as well as queue and double-ended operations.

Whiteboard
Loading diagram...

A useful declaration is:


The variable is declared as Deque<String> because the code needs deque behavior, not implementation-specific features. This makes it easier to replace the implementation later if the requirements change.

ArrayDeque is a concrete class, so it can be instantiated with new. Deque is an interface and cannot be instantiated directly.

3. How ArrayDeque Works Internally

At a high level, ArrayDeque maintains a backing array and indices that identify where the first and next insertion positions are. The exact implementation details may vary between Java releases, but the key concept is the same: a circular buffer reuses array slots by wrapping indices around.

When an element is inserted at the front, the head position moves backward and the element is placed into that slot. When an element is inserted at the tail, the element is written at the tail position and the tail advances. Removing an element clears its slot and advances the relevant index.

Unlike an ordinary array list removal from the front, these operations do not shift every remaining element one position over. That is the main source of the efficient endpoint operations.

The array may have unused slots before the head or after the tail. Those slots are not part of the logical deque. The logical order is determined by walking from the head and wrapping around the end when necessary.

4. Understanding the Circular Buffer

A circular buffer treats the last array slot as adjacent to the first slot. This is a logical arrangement; the physical array remains a normal array in memory.

Imagine a backing array with eight slots. The deque may hold elements in slots 6 and 7, then continue with slots 0 and 1. The elements are physically split across the end of the array, but the logical order is continuous.

Whiteboard
Loading diagram...


The head points to the first logical element. The tail identifies where the next tail insertion can be placed. After the last array slot, the index wraps around to the beginning rather than moving beyond the array.

This arrangement avoids repeated shifting. It also means that the logical first element is not necessarily stored in array slot zero. The deque's iteration and endpoint methods account for the wraparound internally.

5. Adding and Removing Elements as a Queue

For FIFO queue behavior, insert at the tail and remove from the head. The methods most often used are offerLast() and pollFirst(), though offer() and poll() are also available as queue-style operations.


The call to poll() returns Order A, the oldest item still in the queue. The remaining order is Order B followed by Order C.

offer() returns a boolean to indicate whether the insertion succeeded. poll() returns and removes the head, or returns null if the deque is empty. Because ArrayDeque is an unbounded resizable structure under ordinary use, insertion normally succeeds for a non-null element unless a resource failure occurs.

A queue built on ArrayDeque works well for single-threaded tasks, such as processing a list of pending jobs in arrival order. It should not be used as a thread-safe work queue without external synchronization.

6. Using ArrayDeque as a Stack

A stack follows Last In, First Out (LIFO): the newest element is removed first. ArrayDeque supports the stack-style methods push(), pop(), and peek().

MethodBehavior
push(e)Inserts an element at the head
pop()Removes and returns the head; throws if empty
peek()Returns the head without removing it; returns null if empty

Example:


lastAction receives Applied coupon, because that action was pushed most recently. The next pop would return Changed quantity.

This is useful for undo history, expression evaluation, depth-first traversal, and any task where the newest pending item should be handled first.

7. ArrayDeque vs LinkedList

Both classes implement Deque, but their internal structures differ.

ArrayDeque stores element references in an array. LinkedList stores each element in a separate node that links to its previous and next nodes. This difference affects allocation, memory use, and how efficiently the processor can access elements during iteration.

PropertyArrayDequeLinkedList
Backing structureResizable circular arrayDoubly linked nodes
Endpoint operationsO(1) amortizedO(1)
Per-element allocationStored in array slotsA node object for each element
Memory overheadUsually lowerUsually higher due to node links
Cache localityGenerally betterGenerally poorer
Allows nullNoYes
Also implements ListNoYes
Typical deque recommendationDefault choiceUse when its extra behavior is needed

For queue and stack work, ArrayDeque is usually preferred because it avoids allocating a node for every insertion and generally has better cache locality. LinkedList may still be useful when list-specific behavior is also needed.

The two classes expose the same deque methods, so changing between them usually requires changing only the implementation declaration when the rest of the code uses the Deque interface.

8. ArrayDeque vs the Legacy Stack Class

Java's Stack class is a legacy class that extends Vector. Vector synchronizes its methods, which can introduce unnecessary overhead in single-threaded code. This synchronization also does not automatically make a sequence of several stack operations atomic.

ArrayDeque is generally the modern recommendation for ordinary stack behavior. It offers push, pop, and peek, avoids the legacy Vector inheritance design, and is typically faster for stack use.

AspectArrayDequeStack
API familyImplements DequeExtends Vector
Typical useRecommended for new stack codeMainly legacy compatibility
SynchronizationNot synchronizedMethods are synchronized through legacy inheritance
Can also act as a queueYesNot naturally designed for that purpose
Null elementsRejectedDifferent behavior inherited from vector storage

This does not mean ArrayDeque is thread-safe. It is not. If several threads need to access and modify a stack concurrently, choose a suitable concurrent structure or add synchronization that correctly protects the entire operation.

9. Core ArrayDeque Methods

The most important methods can be grouped by where they operate.

MethodMeaning
addFirst(e)Insert at the head; throws if insertion fails
offerFirst(e)Attempt insertion at the head; returns success status
addLast(e)Insert at the tail; throws if insertion fails
offerLast(e)Attempt insertion at the tail; returns success status
removeFirst()Remove and return the head; throws if empty
pollFirst()Remove and return the head; returns null if empty
removeLast()Remove and return the tail; throws if empty
pollLast()Remove and return the tail; returns null if empty
getFirst()Read the head; throws if empty
peekFirst()Read the head; returns null if empty
getLast()Read the tail; throws if empty
peekLast()Read the tail; returns null if empty
push(e)Stack-style insertion at the head
pop()Stack-style removal from the head
peek()Read the head without removing it
size()Return the number of elements
isEmpty()Check whether there are no elements
contains(o)Check whether an equal element is present
clear()Remove all elements

The endpoint methods are the main strength of ArrayDeque. Methods such as contains() are not constant-time because the deque is not a hash-based lookup structure; they may need to scan elements one by one.

10. Exception-Based Methods and Special Return Values

Several deque operations come in two forms. One form throws when the operation cannot be completed; the other returns a special value.

OperationException formSpecial-value form
Insert at headaddFirst(e)offerFirst(e)
Remove from headremoveFirst()pollFirst()
Examine headgetFirst()peekFirst()
Insert at tailaddLast(e)offerLast(e)
Remove from tailremoveLast()pollLast()
Examine tailgetLast()peekLast()

The removal and examination pairs differ most visibly when the deque is empty. For example, removeFirst() throws NoSuchElementException, while pollFirst() returns null.

Use the exception form when the operation is expected to succeed and failure indicates a bug. Use the special-value form when an empty deque is an ordinary state that the code can handle.

Because ArrayDeque rejects null elements, a null result from poll() or peek() unambiguously means that no element was available.

11. Null Elements Are Not Allowed

ArrayDeque does not permit null elements. Inserting null causes a NullPointerException at the point of insertion.

This restriction is intentional. Methods such as poll(), pollFirst(), peek(), and peekFirst() use null to signal that the deque is empty. If null were also allowed as a stored element, callers could not reliably distinguish an actual null element from the absence of an element.

If an application needs to represent “no value,” it is usually clearer to represent that state separately—for example, with a wrapper such as Optional<T>—rather than inserting null into the deque.

This is a useful distinction from LinkedList, which permits null elements. Even though LinkedList allows null, using null as a queue element can make the meaning of a polling result ambiguous. For deque-style processing, avoiding null is generally the simpler design.

12. Capacity and Automatic Growth

ArrayDeque grows automatically when its backing array can no longer hold additional elements. This means callers do not normally need to manage a fixed capacity.

The default constructor creates a deque with a default initial capacity. A constructor that accepts an integer can be used when the approximate number of elements is already known, and a constructor that accepts a collection can initialize the deque from an existing collection.

The array grows when needed, and existing elements are copied into the larger array in logical deque order. That copying operation takes O(n) time for n stored elements. However, it occurs only occasionally, so endpoint operations are O(1) amortized over a sequence of insertions.

A deque that grows large and later becomes empty does not necessarily shrink its backing array automatically. For a long-lived deque whose size fluctuates dramatically, retaining the larger array may matter. If reclaiming that capacity is important, replacing the deque with a fresh instance is one straightforward option.

13. Pre-Sizing for Large Workloads

When the approximate size is known, specifying an initial capacity can reduce the number of resize operations. For example, a batch job expected to process tens of thousands of orders can initialize the deque with a suitable capacity rather than repeatedly growing from a small default array.


Pre-sizing is a performance optimization, not a requirement for correctness. If the estimate is too small, the deque can still grow. If the estimate is much too large, the program may reserve more memory than necessary.

The best capacity depends on the expected workload. Use a reasonable estimate when it is readily available, but avoid tuning every small deque without a measurable reason.

14. Circular Buffer Growth and Amortized Complexity

Most insertions and removals change only an index and one array slot. When the array becomes full, however, the deque must allocate a larger array and copy its existing elements.

This explains the difference between worst-case cost for one operation and amortized cost over many operations:

  • A typical endpoint insertion or removal is O(1).
  • The particular insertion that triggers a resize can take O(n).
  • Across many insertions, the occasional resize is spread over the sequence, so insertion is O(1) amortized.

The circular arrangement is important because it prevents front removals from shifting all remaining elements. A conventional array-based sequence might need to move many elements after removing index zero; ArrayDeque advances the head instead.

15. A Sliding Window of Recent Events

A sliding window keeps only the most recent N elements. This pattern is common in dashboards, recent-view lists, monitoring events, and lightweight rolling histories.

The algorithm is simple:

  1. Add each new event at the tail.
  2. If the deque has more than N elements, remove the oldest element from the head.
  3. Read the deque from head to tail to display the surviving events.
Whiteboard
Loading diagram...


For example, a storefront may keep the last five products viewed during a session. When a sixth product is viewed, the oldest product is removed and the new product is appended. Each endpoint operation is O(1) amortized, so processing n events takes O(n) total time under this pattern.

A LinkedList can implement the same algorithm, but it allocates a node for each element. ArrayDeque is often more memory-efficient for a high-volume rolling sequence.

16. A Work Queue for Order Processing

For a single-threaded workflow, ArrayDeque can hold pending orders in arrival order. The intake portion appends orders to the tail, and the worker removes the next order from the head.

It can also insert an urgent order at the head when the application explicitly wants it to be processed before normal waiting work. Normal orders can continue to enter at the tail.

This should not be confused with a full priority queue. ArrayDeque does not compare priority values or automatically sort orders. It simply follows the order implied by calls such as offerLast() and offerFirst(). If the application needs automatic priority ordering, PriorityQueue may be a better match.

Thread-safety warning: ArrayDeque is not thread-safe. If one thread adds orders while another removes them, use proper synchronization or a concurrent collection such as LinkedBlockingDeque or another suitable BlockingQueue implementation.

17. Choosing ArrayDeque for Breadth-First Search

Breadth-first search (BFS) explores graph or tree nodes level by level. A queue is used to remember which nodes should be visited next. ArrayDeque is a common implementation for this queue in single-threaded algorithms.

The general process is to insert the starting node, remove the head to visit it, then append newly discovered neighbors at the tail. Repeating this process explores nodes in the order they enter the queue.

This is another example of why efficient endpoint operations matter: each node can be added and removed without shifting the rest of the collection. ArrayDeque provides this behavior without the per-node linked-list allocation of LinkedList.

18. Time Complexity of ArrayDeque

OperationTypical time complexityExplanation
offerFirst() / offerLast()O(1) amortizedUpdates an end index and writes a slot, except during occasional growth
pollFirst() / pollLast()O(1) amortizedReads and clears an endpoint slot, then advances an index
push() / pop() / peek()O(1) amortized for modifying operations; O(1) for peekStack methods operate at the head
peekFirst() / peekLast()O(1)Reads an endpoint
size() / isEmpty()O(1)Uses maintained state
contains()O(n)May scan the elements
Remove a particular objectO(n)May need to search for the matching value
ResizeO(n)Copies existing elements into a larger array

The key distinction is that ArrayDeque is optimized for working at the ends. It is not a substitute for HashSet when fast membership testing is the primary requirement, nor is it a sorted structure like TreeSet.

19. Common Mistakes to Avoid

Mistake 1: Expecting automatic priority ordering

Adding an urgent order at the tail does not cause it to jump ahead of other orders. Use offerFirst() when the application deliberately wants front insertion, or use a priority-based data structure if items must be ordered by a priority field.

Mistake 2: Using the wrong end

For FIFO behavior, append at the tail and remove from the head. For stack behavior, push and pop at the head. Mixing these patterns changes the order in which items are processed.

Mistake 3: Assuming every operation is constant-time

Endpoint operations are O(1) amortized, but contains() and removing an arbitrary matching object can take O(n). Resizing can also make a particular insertion O(n).

Mistake 4: Attempting to store null

ArrayDeque rejects null with NullPointerException. Model absence separately instead of treating null as a regular element.

Mistake 5: Treating ArrayDeque as thread-safe

It is not synchronized. Concurrent access that includes modifications requires a suitable concurrency design.

Mistake 6: Choosing LinkedList by default for queues

Although LinkedList supports queue operations, it typically uses more memory per element and has poorer locality. ArrayDeque is usually the better first choice for ordinary queue and stack work.

Mistake 7: Using ArrayDeque for fast arbitrary lookup

The deque's strength is endpoint operations. Searching for a value is linear-time. If fast lookup by key is essential, consider a HashMap or HashSet alongside the deque, depending on the problem.

20. Best Practices

  • Prefer ArrayDeque for ordinary single-threaded queue and stack implementations.
  • Declare the variable as Deque<E> when implementation-specific methods are unnecessary.
  • Use offerLast() and pollFirst() for FIFO processing.
  • Use push(), pop(), and peek() for LIFO stack logic.
  • Use offerFirst() only when the application deliberately needs new work at the head.
  • Avoid null elements.
  • Estimate initial capacity for large known workloads, without over-allocating unnecessarily.
  • Remember that the deque can grow but does not necessarily shrink its backing array after elements are removed.
  • Use a concurrent deque or queue for cross-thread communication.
  • Use a priority-oriented collection when ordering by priority is required.
  • Use a set or map when fast membership lookup is more important than endpoint operations.

21. ArrayDeque vs Other Collections

RequirementRecommended starting pointReason
Single-threaded FIFO queueArrayDequeEfficient operations at both ends
Single-threaded stackArrayDequeModern alternative to legacy Stack
Queue with automatic priority orderingPriorityQueueHead is determined by natural ordering or comparator
Fast membership lookupHashSetHash-based membership checks are O(1) on average
Sorted unique valuesTreeSetMaintains sorted order and supports range queries
Cross-thread producer-consumer queueSuitable BlockingQueueDesigned for concurrent coordination
Need both list and deque behaviorLinkedList, when justifiedImplements List and Deque

Choose based on the operations the application needs, rather than choosing a collection solely because it implements a familiar interface.

22. Quick Revision

  • ArrayDeque is a resizable-array implementation of Deque.
  • A deque supports insertion and removal at both ends.
  • Its circular buffer reuses slots by wrapping indices around the backing array.
  • Normal endpoint operations do not shift all remaining elements.
  • Queue behavior uses tail insertion and head removal.
  • Stack behavior uses push(), pop(), and peek() at the head.
  • Endpoint modifications are O(1) amortized.
  • A resize copies elements and can take O(n).
  • ArrayDeque is generally preferred over LinkedList for ordinary queue work and over legacy Stack for ordinary stack work.
  • It rejects null elements.
  • It is not thread-safe.
  • contains() and arbitrary-object removal are O(n).
  • It does not automatically sort elements or prioritize them.
  • Initial capacity can be specified to reduce resizing when workload size is known.

23. Conclusion

ArrayDeque is a versatile and efficient implementation of the Java Deque interface. Its circular-array design makes insertion and removal at either end efficient without shifting all the remaining elements. The same collection can act as a FIFO queue, a LIFO stack, or a rolling buffer, depending on how the application uses its methods.

For new single-threaded queue and stack code, ArrayDeque is usually the best starting point. Use offerLast() and pollFirst() for FIFO processing, and use push() and pop() for stack behavior. Consider capacity when a large workload is predictable, avoid null elements, and do not assume the class is thread-safe.

The most important design rule is to match the collection to the required behavior. ArrayDeque excels at operations on the ends; use another structure when the problem needs sorted ordering, automatic priority management, fast arbitrary lookup, or coordinated concurrent access.