Back to Blog
Java

java arraydeque: When to Use It

java arraydeque: Learn when to use ArrayDeque in Java, its performance characteristics, and how it compares to LinkedList for queue and stack operations.

ArrayDequeDequeQueueStackCollections
A technical illustration showing the circular buffer structure of Java's ArrayDeque with arrows indicating head and tail operations.

The java arraydeque class implements the Deque interface and provides a resizable array-based implementation of a double-ended queue. Unlike LinkedList, which also implements Deque, ArrayDeque is backed by a circular array that grows automatically as needed. This design has direct consequences for memory layout, iteration speed, and garbage collection pressure, making ArrayDeque the better choice for most queue and stack use cases in Java code that runs on a single thread.

The Core Data Structure Behind ArrayDeque

ArrayDeque stores elements in an array that is treated as circular. Two indices, head and tail, mark the logical ends of the deque. When an element is added at either end, the index moves forward or backward in the circular buffer, wrapping around when it reaches the array boundary. When the array becomes full, the deque allocates a new array roughly double the previous size and copies the logical elements into it, preserving order.

This internal circular-buffer design explains several behaviors observers see when working with ArrayDeque:

  • Adding or removing at either end takes constant amortized time because no shifting of remaining elements is required.
  • Iteration follows the logical order from head to tail, not the raw array order.
  • The implementation does not allow null elements, because null is used internally to detect the empty slots and the head/tail boundaries.

Consider a straightforward example that pushes elements onto both ends:

import java.util.ArrayDeque; import java.util.Deque; Deque<String> deque = new ArrayDeque<>(); deque.addFirst("first"); deque.addLast("last"); deque.addFirst("newFirst"); deque.forEach(System.out::println);

The output is newFirst, first, last. The circular array holds elements in a physically scattered order, but iteration uses the logical head and tail indices to produce the correct sequence.

Comparing ArrayDeque with LinkedList

The most common decision developers face is choosing between ArrayDeque and LinkedList for a deque, queue, or stack. Both implement Deque<E>, but their internal structures lead to different tradeoffs.

AspectArrayDequeLinkedList
Underlying structureResizable circular arrayDoubly-linked node chain
Memory per elementArray slot per elementNode object plus references
Random accessNot supportedNot supported
Null elementsNot allowedAllowed
CPU cache localityHigher, because elements are stored contiguouslyLower, because nodes are scattered across memory
Add/remove at endsConstant amortized timeConstant time, but with node allocation overhead
Suitable for stacks/queuesYes, and usually preferableYes, but with more overhead

For a typical queue or stack in a single-threaded program, ArrayDeque generally wins because it avoids the per-node object allocation of LinkedList. Each element stored in LinkedList requires a Node object containing the element, a reference to the next node, and a reference to the previous node. That overhead can be significant when you process millions of elements. ArrayDeque stores only the element itself in an array slot, reducing memory footprint and improving cache locality because the elements are physically adjacent in memory.

LinkedList still has a place when you need to insert or remove elements in the middle of the list efficiently, because it can locate a known node and relink neighbors in constant time. However, if your operations are limited to the two ends, ArrayDeque is the more appropriate data structure.

Using ArrayDeque as a Stack

The Deque interface provides stack operations push, pop, and peek, which throw exceptions on empty conditions. The ArrayDeque class implements these directly, giving you a stack that is often preferable to the legacy Stack class. Stack extends Vector, meaning every operation is synchronized, which carries unnecessary locking overhead in single-threaded scenarios. ArrayDeque has no such synchronization, making it faster in typical use.

import java.util.ArrayDeque; import java.util.Deque; Deque<String> stack = new ArrayDeque<>(); stack.push("first"); stack.push("second"); stack.push("third"); while (!stack.isEmpty()) { System.out.println(stack.pop()); }

The output is third, second, first, demonstrating LIFO order. Notice that push adds to the head of the deque, and pop removes from the head. This is exactly the stack semantics you expect.

If you attempt to pop from an empty ArrayDeque, it throws NoSuchElementException. For cases where an empty stack is a routine condition, use pollFirst() or peekFirst() instead; these return null rather than throwing.

Using ArrayDeque as a Queue

For a FIFO queue, use addLast() / offerLast() to enqueue and pollFirst() / removeFirst() to dequeue. The non-throwing variants (offer, poll, peek) return null on failure, which is convenient when a queue might be empty during normal operation.

import java.util.ArrayDeque; import java.util.Deque; Deque<String> queue = new ArrayDeque<>(); queue.add("task1"); queue.add("task2"); queue.add("task3"); String next = queue.poll(); // returns "task1"

Because ArrayDeque implements Deque, it also acts as a double-ended queue. You can add or remove from both ends with addFirst, addLast, pollFirst, pollLast, and so on. This flexibility is useful when implementing algorithms like sliding window maximum, where you need to discard elements from the front and add new ones at the back while maintaining order.

Performance Characteristics and Iteration

Operations at either end of an ArrayDeque run in amortized constant time. The resizing operation, when the array must grow, takes O(n) time because it copies all elements to the new array. However, resizing is infrequent, and the amortized cost remains O(1) per operation. This is the same reasoning used for other resizable array structures like ArrayList.

Iteration over an ArrayDeque is direct and does not require traversing node references. The iterator walks through the circular buffer logically, so iterating over N elements is O(N) with good cache behavior. In contrast, iterating a LinkedList requires following references, which can cause cache misses on every node.

The memory footprint per element is also smaller. Each element in an ArrayDeque occupies a single slot in an object array, typically 4 or 8 bytes for a reference. Each element in a LinkedList requires a Node object that references the element and two neighboring nodes, creating substantial per-element overhead.

Because ArrayDeque does not allow null, you must avoid inserting null if you expect to use poll or peek to check for empty queues. If your data legitimately contains null, either wrap it in a companion object or use a different structure.

Thread Safety and Concurrency Limits

ArrayDeque is not thread-safe. If multiple threads access the same instance concurrently, you must synchronize externally or use a concurrent collection. For single-producer, single-consumer patterns, java.util.concurrent.ConcurrentLinkedDeque is an alternative, but it has different performance characteristics. For a blocking queue where producers wait when full, use LinkedBlockingDeque or ArrayBlockingQueue.

The lack of synchronization in ArrayDeque is often an advantage in single-threaded code. You do not pay for locks you never need. If you later introduce multiple threads, wrap the deque with Collections.synchronizedDeque() to make it safe, but understand that compound operations like isEmpty() followed by poll() still require external locking to remain atomic.

Practical Pitfalls and Edge Cases

The most common mistake with ArrayDeque is treating it as a general-purpose list. It does not support indexed access like get(int), and it does not implement List. You cannot iterate with a plain for loop using an index. Use the enhanced for loop or an Iterator instead.

Another pitfall is assuming ArrayDeque is shrinkable. The array grows as needed, but once it grows, it never shrinks automatically even after elements are removed. If you repeatedly add and remove a large number of elements, the internal array stays at its largest size. This is not usually a problem in applications that process data in predictable phases, but it can inflate memory usage if a deque experiences a burst of adds followed by a long period of low activity.

Consider the removeFirstOccurrence method, which removes the first occurrence of a specified element. It is O(n), as it must scan the deque to find the element. If you need frequent removal by value, a HashMap or LinkedHashMap may be more appropriate.

Finally, be aware that ArrayDeque iterators are fail-fast: if the deque is structurally modified after the iterator is created, the iterator throws ConcurrentModificationException. This is consistent with most Java collections and should remind you not to modify the deque while iterating differently than via the iterator's own remove() method.```java import java.util.ArrayDeque; import java.util.Deque;

public class SlidingWindowMax { public static int[] maxSlidingWindow(int[] nums, int k) { if (nums.length == 0) return new int[0]; int[] result = new int[nums.length - k + 1]; Deque<Integer> deque = new ArrayDeque<>(); // indices

    for (int i = 0; i < nums.length; i++) {
        // Remove indices outside the window
        while (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
            deque.pollFirst();
        }
        // Remove indices whose values are less than current
        while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
            deque.pollLast();
        }
        deque.addLast(i);

        if (i >= k - 1) {
            result[i - k + 1] = nums[deque.peekFirst()];
        }
    }
    return result;
}

}


This algorithm keeps candidate maximum indices in the deque, sorted from largest value at the front to smallest at the back. The front always holds the index of the current window's maximum. Because each index is added and removed at most once, the total time is O(n). This pattern relies on the ability to add and remove from both ends efficiently, which `ArrayDeque` provides.

## Compatibility and API Boundaries

`ArrayDeque` has been available since Java 6, so it is present in nearly every production Java runtime. However, it is not a `List`, so you cannot pass it to methods that require a `List`. If your API accepts a `Collection`, `ArrayDeque` works because it implements `Collection`. If you need random access, convert it to an `ArrayList` first.

The `Deque` interface includes both exception-throwing methods (`addFirst`, `removeFirst`, `getFirst`) and non-throwing alternatives (`offerFirst`, `pollFirst`, `peekFirst`). Which set you choose depends on whether the empty case is an exceptional condition or a normal control flow. Using the non-throwing variants for normal empty checks avoids unnecessary exception overhead.

When integrating with legacy code, you may encounter older stack or queue APIs that expect `Stack` or `Vector`. You can still use `ArrayDeque` internally and convert when necessary, but be aware that such conversions copy elements and cost O(n). In most cases, it is cleaner to update the surrounding code to work with `Deque` directly.
java arraydeque - When and How to Use It | RYUSLOG DEV