Back to Blog
Java

Java Stack vs Deque: Choosing the Right LIFO Structure

java stack vs deque: Compare Java's legacy Stack class with the Deque interface for LIFO operations, covering 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 java stack vs deque decision comes down to one question: should you use the legacy Stack class or the Deque interface? For new code, Deque with ArrayDeque is almost always the better choice, but 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 enforces the LIFO discipline.

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

Stack synchronizes every method because Vector does. 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 use, ConcurrentLinkedDeque or a Deque wrapped with Collections.synchronizedDeque are more appropriate, depending on whether you need blocking behavior.

The performance difference between Stack and ArrayDeque in single-threaded code comes from two sources: the lock acquisition in Stack and the array-based storage in ArrayDeque, which has better cache locality than Vector's synchronized array. The exact magnitude depends on the JVM, the workload, and whether the lock is contended, 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 practice, 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