Back to Blog
Java

Java Stack Class: Usage, Tradeoffs, and Examples

java stack class: Understand the java.util.Stack class, its LIFO mechanics, synchronization overhead, and when to prefer ArrayDeque for performance-sensitive code.

StackLast-In-First-OutJava CollectionsDequeArrayDeque
Illustration showing a stack data structure with elements pushed and popped in LIFO order, highlighting the Java Stack class.

The java stack classjava.util.Stack—is one of the oldest collection types in Java. It extends Vector and implements List, which means it inherits all of Vector's methods, including legacy operations that have little to do with stack semantics. For developers learning about LIFO (last-in, first-out) behavior, Stack is often the first implementation encountered, but its design carries historical baggage that makes it a poor default choice in modern Java.

What the Stack Class Actually Provides

Stack provides five core methods that define its LIFO contract:

  • push(E item) — adds an item to the top of the stack
  • pop() — removes and returns the top item
  • peek() — returns the top item without removing it
  • empty() — returns true if the stack has no elements
  • search(Object o) — returns the 1-based position of the element from the top

Because Stack extends Vector, it also exposes methods like add(int index, E element), get(int index), and remove(int index). These List methods are inherited and can be called on a Stack, but they violate the intended LIFO contract. You could, for example, insert an element at the bottom of the stack using add(0, element), which completely changes the semantics. This is not a bug; it is a design flaw inherited from Vector. Any code that relies on strict stack behavior must avoid these inherited methods.

Core Stack Operations in Practice

A typical use of the Stack class looks like this:

import java.util.Stack; Stack<String> stack = new Stack<>(); stack.push("first"); stack.push("second"); stack.push("third"); String top = stack.peek(); // "third" System.out.println(top); String removed = stack.pop(); // "third" System.out.println(removed); // stack now contains ["first", "second"]

The peek method does not modify the stack; it only returns the top element. pop removes it. Both throw EmptyStackException if the stack is empty. Unlike many other collection methods that return null or an optional, Stack follows the older convention of throwing an unchecked exception.

The search method returns the 1-based position of an element from the top. The top element has position 1. If the element is not present, it returns -1.

Stack<String> stack = new Stack<>(); stack.push("a"); stack.push("b"); stack.push("c"); int pos = stack.search("b"); // 2 because "c" is at position 1 System.out.println(pos);

When Does EmptyStackException Occur?

Calling peek() or pop() on an empty Stack throws EmptyStackException. This is an unchecked exception that extends RuntimeException, so the compiler does not force you to catch it. If you do not guard against it, your application will crash when the stack is empty. Consider this example:

Stack<Integer> stack = new Stack<>(); int value = stack.pop(); // throws EmptyStackException

To avoid this, check empty() before popping, or wrap the operation in a try-catch block. The exception message is often null, which makes debugging harder. In concurrent scenarios, checking empty() and then popping is not atomic; another thread could pop between the check and the pop. If you need thread safety, synchronize externally or use a thread-safe alternative.

Why Vector Inheritance Is a Problem

Because Stack extends Vector, every method in Stack is synchronized. This means each push, pop, and peek acquires a lock on the instance. In single-threaded applications, this synchronization adds unnecessary overhead. The Java documentation itself has discouraged the use of Stack in favor of ArrayDeque. The ArrayDeque class implements the Deque interface and provides push, pop, and peek methods, but it is not synchronized. For most use cases, ArrayDeque is faster and does not expose list operations.

Another consequence of extending Vector is that Stack can be freely treated as a List. You can pass a Stack to a method that accepts a List and then modify it using list methods. This breaks encapsulation and makes the code less predictable. If you want a data structure that only supports LIFO behavior, you are better off using ArrayDeque and storing it in a Deque variable.

Comparing Stack with Deque Implementations

The Java Collections Framework includes the Deque interface, which defines a double-ended queue. Both ArrayDeque and LinkedList implement it. The Deque interface provides the same push, pop, and peek methods as Stack, plus additional methods like offerFirst and pollLast. Because ArrayDeque does not extend Vector, it does not inherit random access methods. This makes it a more faithful LIFO implementation.

Featurejava.util.StackArrayDeque
SynchronizationSynchronized methodsNot synchronized
LIFO methods (push, pop, peek)YesYes
Inherits random accessYes (via Vector)No
Allows null elementsYesNo (throws NullPointerException)
Iterator behaviorFail-fastFail-fast

ArrayDeque does not allow null elements. The Deque interface explicitly states that null elements are not permitted. If your data may contain nulls, you must use Stack or another implementation that allows them. However, in most stack usage, null elements are not common, and the restriction is rarely a problem.

Synchronization: What Is Actually Thread-Safe

Because every Stack method is synchronized, multiple threads can safely call push and pop on the same instance without additional synchronization. However, this does not make compound operations atomic. For example, checking empty() and then calling pop() is not thread-safe. The same applies to iterating over a Stack while another thread modifies it. The iterator will throw ConcurrentModificationException if the stack is modified after the iterator is created.

If you need a thread-safe stack with stronger atomic operations, consider using ConcurrentLinkedDeque (which implements Deque). It is thread-safe and lock-free, but it does not allow null elements. Alternatively, you can synchronize externally on the stack instance or use a lock to protect compound sequences. In practice, Stack's method-level synchronization provides a false sense of security; it forces every single method call to acquire a lock, but it does not protect logical sequences.

A Practical Example: Undo/Redo Implementation

A classic use of a stack is an undo/redo system. Suppose you are building a text editor or a graphical application. You can maintain a stack of actions that can be undone. Here is a simplified implementation using Deque with ArrayDeque:

import java.util.ArrayDeque; import java.util.Deque; public class UndoHistory { private Deque<String> undoStack = new ArrayDeque<>(); private Deque<String> redoStack = new ArrayDeque<>(); public void performAction(String action) { undoStack.push(action); redoStack.clear(); // new action invalidates redo history } public String undo() { if (undoStack.isEmpty()) { return null; } String action = undoStack.pop(); redoStack.push(action); return action; } public String redo() { if (redoStack.isEmpty()) { return null; } String action = redoStack.pop(); undoStack.push(action); return action; } }

This example uses ArrayDeque instead of Stack because the LIFO behavior is identical and the code does not need synchronization or random access. If you need to store null actions, you would have to switch to Stack or add a wrapper object. In most domains, actions are non-null, so ArrayDeque is the natural choice.

Memory and Performance Considerations

Stack is backed by an array that grows dynamically when it reaches its capacity. When the array is full and you push a new element, the class allocates a new array, copies the old elements, and then adds the new element. This copying is O(n) for that push operation, but the amortized cost of push remains O(1). ArrayDeque is also array-backed and has the same amortized O(1) push/pop behavior. The main performance difference comes from synchronization. In a single-threaded application, ArrayDeque avoids the lock overhead, which can be significant in tight loops.

If you are writing code that is performance-sensitive, such as a parser or a recursive-descent interpreter, you should measure with and without synchronization. But as a general rule, the lack of a lock in ArrayDeque makes it faster in virtually all single-threaded scenarios. The Java documentation explicitly recommends using ArrayDeque in preference to Stack when you need a LIFO stack.

Compatibility and Migration

Because Stack has been part of Java since version 1.0, it is unlikely to be removed. Code that uses Stack will continue to compile and run in current and future JDKs. However, new code should prefer the Deque interface. If you have an existing Stack that you want to migrate, the change is usually straightforward: replace the import and the variable type.

Before:

import java.util.Stack; Stack<String> stack = new Stack<>();

After:

import java.util.ArrayDeque; import java.util.Deque; Deque<String> stack = new ArrayDeque<>();

Because both provide the same push, pop, and peek methods, the rest of the code often stays the same. One subtle difference is the return type of peek: in Stack, peek returns the top element or throws EmptyStackException; in Deque, peek returns the top element or returns null if the deque is empty. Similarly, pop in Deque throws NoSuchElementException instead of EmptyStackException. If your code catches EmptyStackException, you must change the catch block to NoSuchElementException or catch RuntimeException.

Another difference is search. Deque does not provide a search method. If your code uses search to find an element's position from the top, you need to implement that logic manually, for example by iterating through the deque. Such a use is rare and usually indicates that the stack is being used for something other than pure LIFO behavior.

Edge Cases: Null Elements and Iteration

As mentioned, Stack allows null elements, whereas ArrayDeque does not. If your stack can contain nulls, you must be careful when migrating. For example:

Stack<String> stack = new Stack<>(); stack.push(null); // allowed String value = stack.pop(); // returns null

The equivalent with ArrayDeque throws NullPointerException at push. If null is a valid value in your domain, you either stay with Stack or wrap null in a sentinel object. In practice, storing null in a stack is often a code smell, but there are cases where it is used to represent a missing value, especially when the stack is holding results from a method that can return null.

Iterating over a Stack using iterator() produces an enumeration that traverses from the top to the bottom. ArrayDeque iterates from the first element (the head) to the last. For a LIFO stack, the top is the head of the deque, so iteration order is the same. But if you iterated over a Stack using a ListIterator, it would behave differently because of random access.

When to Use the Stack Class Today

Given all the tradeoffs, the java.util.Stack class is primarily a legacy type. You should use it in these limited situations:

  • You need a LIFO stack that allows null elements.
  • You are working with legacy code that already exposes Stack and you are unable to change the interface.
  • You intentionally rely on the fact that Stack is a List and need to pass it to a method that accepts a List without adapting it.
  • You need thread safety and you accept the overhead of synchronized methods, without needing atomic compound operations.

For all new development, prefer the Deque interface with an ArrayDeque implementation. This gives you the same LIFO behavior, better performance in single-threaded code, and a cleaner API. The Stack class will not disappear, but it should not be your first choice.

A Deeper Look: Behavior When Using Vector Methods

Because Stack extends Vector, you can call methods that are not stack-like. This can produce surprising bugs. Consider this code:

Stack<String> stack = new Stack<>(); stack.push("a"); stack.push("b"); stack.add(0, "c"); // inserts at the bottom System.out.println(stack.pop()); // prints "b", not "c"

The add(0, "c") method inserts the element at the bottom of the vector, so the LIFO order is not preserved. If you are debugging a problem in existing code that uses Stack, always check whether any of the inherited List methods are being called somewhere. These calls can change the internal order and make the stack behavior unpredictable.

Another inherited method is set(int index, E element), which replaces an element at a specific index. Similarly, remove(int index) removes an element from the middle. These operations are not available on Deque, which is a strong argument for using ArrayDeque.

Final Code Example: A Basic Expression Evaluator

To illustrate the practical use of a stack, here is a simple expression evaluator that uses a Deque to check balanced parentheses, a common interview question and also a real parsing need.

import java.util.ArrayDeque; import java.util.Deque; public class ParenthesesChecker { public static boolean isBalanced(String expression) { Deque<Character> stack = new ArrayDeque<>(); for (char ch : expression.toCharArray()) { if (ch == '(' || ch == '{' || ch == '[') { stack.push(ch); } else if (ch == ')' || ch == '}' || ch == ']') { if (stack.isEmpty()) { return false; } char open = stack.pop(); if (!matches(open, ch)) { return false; } } } return stack.isEmpty(); } private static boolean matches(char open, char close) { return (open == '(' && close == ')') || (open == '{' && close == '}') || (open == '[' && close == ']'); } public static void main(String[] args) { System.out.println(isBalanced("({[]})")); // true System.out.println(isBalanced("([)]")); // false } }

This example uses ArrayDeque because it is the recommended implementation. The push and pop methods are O(1), and the code is clear. If you replace Deque<Character> stack = new ArrayDeque<>() with Stack<Character> stack = new Stack<>(), the logic remains the same, except that pop throws EmptyStackException when the stack is empty, but this code checks isEmpty() first. The matches method is a simple helper to ensure the closing bracket corresponds to the opening one.

The java stack class is not inherently wrong, but it is over-engineered for most uses. Its synchronized methods and inherited List functionality do more harm than good in modern Java applications. Choose ArrayDeque unless you have a specific reason to use Stack, and your code will be shorter, faster, and safer to maintain.

java stack class: Practical Usage and Code Examples | RYUSLOG DEV