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.
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.
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().
| Method | Behavior |
|---|---|
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.
| Property | ArrayDeque | LinkedList |
|---|---|---|
| Backing structure | Resizable circular array | Doubly linked nodes |
| Endpoint operations | O(1) amortized | O(1) |
| Per-element allocation | Stored in array slots | A node object for each element |
| Memory overhead | Usually lower | Usually higher due to node links |
| Cache locality | Generally better | Generally poorer |
| Allows null | No | Yes |
Also implements List | No | Yes |
| Typical deque recommendation | Default choice | Use 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.
| Aspect | ArrayDeque | Stack |
|---|---|---|
| API family | Implements Deque | Extends Vector |
| Typical use | Recommended for new stack code | Mainly legacy compatibility |
| Synchronization | Not synchronized | Methods are synchronized through legacy inheritance |
| Can also act as a queue | Yes | Not naturally designed for that purpose |
| Null elements | Rejected | Different 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.
| Method | Meaning |
|---|---|
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.
| Operation | Exception form | Special-value form |
|---|---|---|
| Insert at head | addFirst(e) | offerFirst(e) |
| Remove from head | removeFirst() | pollFirst() |
| Examine head | getFirst() | peekFirst() |
| Insert at tail | addLast(e) | offerLast(e) |
| Remove from tail | removeLast() | pollLast() |
| Examine tail | getLast() | 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:
- Add each new event at the tail.
- If the deque has more than N elements, remove the oldest element from the head.
- Read the deque from head to tail to display the surviving events.
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
| Operation | Typical time complexity | Explanation |
|---|---|---|
offerFirst() / offerLast() | O(1) amortized | Updates an end index and writes a slot, except during occasional growth |
pollFirst() / pollLast() | O(1) amortized | Reads and clears an endpoint slot, then advances an index |
push() / pop() / peek() | O(1) amortized for modifying operations; O(1) for peek | Stack 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 object | O(n) | May need to search for the matching value |
| Resize | O(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
ArrayDequefor ordinary single-threaded queue and stack implementations. - Declare the variable as
Deque<E>when implementation-specific methods are unnecessary. - Use
offerLast()andpollFirst()for FIFO processing. - Use
push(),pop(), andpeek()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
| Requirement | Recommended starting point | Reason |
|---|---|---|
| Single-threaded FIFO queue | ArrayDeque | Efficient operations at both ends |
| Single-threaded stack | ArrayDeque | Modern alternative to legacy Stack |
| Queue with automatic priority ordering | PriorityQueue | Head is determined by natural ordering or comparator |
| Fast membership lookup | HashSet | Hash-based membership checks are O(1) on average |
| Sorted unique values | TreeSet | Maintains sorted order and supports range queries |
| Cross-thread producer-consumer queue | Suitable BlockingQueue | Designed for concurrent coordination |
| Need both list and deque behavior | LinkedList, when justified | Implements 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
ArrayDequeis a resizable-array implementation ofDeque.- 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(), andpeek()at the head. - Endpoint modifications are O(1) amortized.
- A resize copies elements and can take O(n).
ArrayDequeis generally preferred overLinkedListfor ordinary queue work and over legacyStackfor 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.