Back to Blog
Java

Java Stack vs Deque: Choosing the Right LIFO Structure

Compare Java's legacy Stack class with the Deque interface for LIFO operations, including API differences, performance, and migration guidance.

JavaDequeStackLIFOArrayDequeCollections
A visual comparison of Java's Stack class and Deque interface showing LIFO push and pop operations with ArrayDeque as the recommended replacement.

When you need last-in-first-out (LIFO) behavior in Java, the choice between the legacy Stack class and the Deque interface is straightforward: for new code, Deque with ArrayDeque is almost always the better choice. Understanding why requires looking at how Stack behaves and what problems it inherits from Vector.

Why Stack Carries Legacy Baggage

Stack extends Vector, a synchronized resizable array class that dates back to Java 1.0. Because of this inheritance, Stack exposes methods that have nothing to do with LIFO semantics. You can call add(int index, E element), get(int index), or remove(int index) on a Stack, which means callers can bypass the stack discipline entirely.

This is more than a design smell. If you pass a Stack to a method that expects a List, that method can insert elements at arbitrary positions, violating the LIFO contract that the rest of your code relies on. The Stack API also includes search(Object o), which returns a position from the top, and peek(), which throws EmptyStackException on an empty stack rather than returning null or an Optional.

Deque Provides a Clean LIFO Contract

The Deque interface, added in Java 6, was designed to replace Stack. It defines the exact operations you need for LIFO behavior: push(E e), pop(), and peek(). The naming matches Stack's methods, so migrating existing code is mostly mechanical.

Deque<String> stack = new ArrayDeque<>(); stack.push("first"); stack.push("second"); String top = stack.peek(); // "second" String removed = stack.pop(); // "second"

ArrayDeque, the most common implementation, is backed by a resizable circular array. It does not implement List, so callers cannot accidentally treat it as an indexed collection. The type system prevents the indexed access that Stack inherits from List.

Comparing the Same Operations

Here is the same LIFO sequence implemented with both types:

// Legacy Stack Stack<String> legacyStack = new Stack<>(); legacyStack.push("request-1"); legacyStack.push("request-2"); String next = legacyStack.pop(); boolean empty = legacyStack.isEmpty();
// Deque Deque<String> dequeStack = new ArrayDeque<>(); dequeStack.push("request-1"); dequeStack.push("request-2"); String next = dequeStack.pop(); boolean empty = dequeStack.isEmpty();

The calling code is nearly identical. The difference appears when you look at what each type allows beyond these three methods. Stack permits get(0), set(0, ...), and add(0, ...) because it is a List. Deque restricts you to queue and stack operations, which is exactly what you want when the data structure is meant to behave like a stack.

Synchronization and Performance Differences

Every Stack operation carries synchronization overhead because the class is built on the synchronized methods of Vector. In a single-threaded application, that synchronization is pure overhead: every push, pop, and peek acquires and releases a lock even though no concurrent access exists. ArrayDeque has no such overhead.

This does not mean ArrayDeque is thread-safe. It is not. If you need a thread-safe stack, Stack is not the right answer either, because its per-method synchronization does not provide compound-operation safety. A sequence of push followed by pop is not atomic. For concurrent non-blocking use, either ConcurrentLinkedDeque or a Deque wrapped with Collections.synchronizedDeque is appropriate. These choices make individual operations safe, but compound sequences such as isEmpty() followed by pop() still require external synchronization. If you need blocking behavior, use LinkedBlockingDeque, which is a BlockingDeque implementation.

The performance difference between Stack and ArrayDeque in single-threaded code comes mainly from the lock acquisition in Stack; ArrayDeque is an array-backed structure designed for stack-style access. The exact magnitude depends on the JVM and the workload, so treat any specific benchmark number with caution. What is certain is that ArrayDeque removes a source of overhead that Stack cannot avoid.

Null Elements and Failure Behavior

Stack allows null elements. ArrayDeque does not; it throws NullPointerException on push(null). This is a deliberate design decision. A null value in a stack is almost always a bug, and failing fast is better than silently storing a value that downstream code will misinterpret.

peek() and pop() on an empty Stack throw EmptyStackException. ArrayDeque's peek() returns null on an empty deque, while pop() throws NoSuchElementException. If your code relies on EmptyStackException specifically, catching it will not work with Deque without adaptation. In non-concurrent code, checking isEmpty() before calling pop() is the more robust pattern for both types.

When Stack Still Appears in Real Code

Stack is not deprecated, and you will encounter it in older codebases, in code that mirrors legacy library APIs, and in some algorithm textbooks that predate Deque. If you are maintaining such code, there is no urgent need to rewrite it. The class works, and its synchronization, while wasteful, is rarely a bottleneck unless the stack is on a hot path.

When you do migrate, the change is mechanical: replace Stack<String> with Deque<String> and instantiate new ArrayDeque<>(). The push, pop, and peek calls carry over unchanged. The behavioral differences to watch are the exception type on empty pop(), and the fact that ArrayDeque rejects null.

Choosing Between ArrayDeque and LinkedList

Within the Deque interface, you have two common implementations. ArrayDeque is backed by a circular array and is the default choice for stack usage. It offers amortized O(1) push and pop, and iteration is cache-friendly. LinkedList implements both Deque and List, which means it can be used as a stack but also exposes indexed access. Its nodes are allocated individually, so it uses more memory per element and has worse locality. Use LinkedList only when you also need List operations on the same object, or when you need to insert and remove at both ends without array resizing.

For a pure stack, ArrayDeque is the recommended implementation. The Deque interface keeps your code flexible, so if a future requirement calls for a different implementation, the calling code does not change.

Java Stack vs Deque: Practical Usage and Code Examples | RYUSLOG DEV