Skip to content
CalliCoder

Java Stack Class Tutorial with Examples

Java 12 min read

Why iterating a Stack gives you the elements in the wrong order, what extending Vector actually costs, why search() is not an index, and the ArrayDeque replacement that fixes all three.

java.util.Stack is in the JDK, it compiles, and the Javadoc has recommended against it since 2004. That is an awkward combination: the class works, so nothing forces you off it, and the ways it misbehaves are quiet ones: an iteration that comes out backwards, a lock you pay for and cannot use.

This covers what Stack does, the three behaviours that surprise people, and the replacement. Written against Java 17.

The five methods

MethodEffectOn empty
push(e)adds to the top, returns e
pop()removes and returns the topEmptyStackException
peek()returns the top without removingEmptyStackException
empty()true when there are no elements
search(o)1-based distance from the topreturns -1
Stack<String> stack = new Stack<>();

stack.push("a");
stack.push("b");
stack.push("c");

stack.peek();   // "c"
stack.pop();    // "c"
stack.pop();    // "b"
stack.empty();  // false — "a" is still there

Nothing there is surprising. The surprises come from what Stack inherits.

It is a Vector, and that leaks

Stack extends Vector, so a stack is also a resizable list, and every List method is part of its public surface:

Stack<String> stack = new Stack<>();
stack.push("a");
stack.push("b");

stack.add(0, "sneaked");   // legal — inserts at the bottom
stack.get(0);              // "sneaked"
stack.remove(1);           // legal — removes from the middle

A type whose whole purpose is to permit two operations permits twenty. That matters less in code you control than in code you hand to someone else: the signature Stack<Task> promises a discipline the type does not enforce.

The inheritance also fixes the growth policy. Vector doubles its array when full, which is the same thing ArrayList does, so the cost is amortised and fine. It is the other inherited property that costs you.

Iteration runs bottom to top

This is the bug worth remembering, because it produces wrong output rather than an exception:

Stack<String> stack = new Stack<>();
stack.push("first");
stack.push("second");
stack.push("third");

for (String s : stack) {
    System.out.println(s);   // first, second, third
}

System.out.println(stack);   // [first, second, third]

Popping gives third, second, first. Iterating gives the exact opposite. So does toString(), so does stream(), and so does anything else built on the iterator — forEach, List.copyOf, String.join.

The reason is mechanical. push appends at the end of the backing array, and Vector’s iterator walks indices 0 upward. Nobody overrode it. A log line that prints a stack, an undo history rendered into a UI list, a test that asserts on toString() — all three read plausibly and all three are backwards.

If you are stuck with Stack and need pop order, walk it by index in reverse or drain a copy:

for (int i = stack.size() - 1; i >= 0; i--) {
    System.out.println(stack.get(i));
}

search() returns a distance, not an index

Stack<String> stack = new Stack<>();
stack.push("a");   // bottom
stack.push("b");
stack.push("c");   // top

stack.search("c");      // 1  — top of the stack
stack.search("a");      // 3  — three from the top
stack.search("zzz");    // -1 — absent
stack.indexOf("c");     // 2  — inherited from Vector, 0-based from the bottom

Two lookup methods on the same object, counting from opposite ends, one 1-based and one 0-based. search returning 1 for the top rather than 0 also means the familiar if (idx >= 0) test is wrong here — the check is != -1, and search(x) == 1 is the “is this on top” question.

search is a linear scan that uses equals, so it is O(n), not a cheap membership test.

Synchronised, and still not thread-safe

Every Vector method is synchronized, so Stack inherits per-method locking. You pay for it on every single-threaded push. What you get in return does not solve the problem people expect it to:

// Two threads, both reach here with one element left.
if (!stack.empty()) {
    Task t = stack.pop();   // one of them throws EmptyStackException
}

empty() is atomic. pop() is atomic. The sequence is not, and that is the operation you actually performed. Compound actions need a lock held across both calls:

synchronized (stack) {
    if (!stack.empty()) {
        process(stack.pop());
    }
}

At which point the built-in synchronisation is doing nothing except costing you. This is the same check-then-act shape covered under thread interference — per-method locking never composes.

Under real contention the interesting question is which structure you want, not which one has locks bolted on. ConcurrentLinkedDeque and LinkedBlockingDeque are the lock-free and blocking answers respectively; Stack is neither.

Use ArrayDeque

The Javadoc’s own recommendation, and the mapping is direct:

StackArrayDequeDifference
push(e)push(e)same
pop()pop()same — both throw when empty
peek()peek()ArrayDeque returns null instead of throwing
empty()isEmpty()name only
search(o)no equivalent; use contains
Deque<String> stack = new ArrayDeque<>();

stack.push("first");
stack.push("second");
stack.push("third");

for (String s : stack) {
    System.out.println(s);   // third, second, first — pop order
}

while (!stack.isEmpty()) {
    process(stack.pop());
}

Iteration matches pop order, there is no List API to reach around the abstraction, and there is no per-call lock. ArrayDeque also rejects null, which is what makes the poll-based drain unambiguous — the same property that makes it the right Queue implementation.

Declare the variable as Deque, not ArrayDeque. The interface is the contract; you may want ConcurrentLinkedDeque later.

Two things to know before switching. ArrayDeque is not thread-safe at all — no lock, not even the useless one. And peek() returning null on empty means a stack that legitimately holds no elements looks the same as an error, so keep the isEmpty() guard rather than testing the result.

A worked example

Bracket matching is the canonical use, and it exercises the empty case, which is where these get written wrong:

static boolean balanced(String input) {
    Deque<Character> stack = new ArrayDeque<>();
    Map<Character, Character> pairs = Map.of(')', '(', ']', '[', '}', '{');

    for (char c : input.toCharArray()) {
        if (c == '(' || c == '[' || c == '{') {
            stack.push(c);
        } else if (pairs.containsKey(c)) {
            // A closer with nothing open is unbalanced — check before popping.
            if (stack.isEmpty() || stack.pop() != pairs.get(c)) {
                return false;
            }
        }
    }
    return stack.isEmpty();   // anything left open is unbalanced
}

The two isEmpty() calls are both load-bearing. Without the first, ")" throws NoSuchElementException; without the second, "(" returns true.

When a stack is the wrong tool entirely

An explicit stack is worth reaching for when recursion would blow the call stack — a deep tree walk, a graph traversal over a large component. Converting recursion to iteration with an ArrayDeque of frames trades a fixed thread stack for the heap.

For undo history, a stack is usually not enough on its own: users expect redo, which is a second stack, and a bounded history, which ArrayDeque will not enforce for you. ArrayDeque grows without limit; if the history has a cap, that is your code’s job.

More Java collection walkthroughs are in the Java guides.

Frequently asked questions

Is java.util.Stack deprecated?

No — it is not marked @Deprecated and it will keep compiling. The Javadoc recommends Deque instead, which is a stronger hint than most people treat it as, but existing code is under no pressure to change.

Why does printing a Stack show the elements in reverse?

It is not reversed — it is in insertion order, bottom first, because toString() comes from Vector and walks the backing array from index 0. Pop order is the opposite. Both are consistent; only one is what you expected.

Does iterating with stream() fix the order?

No. stream() is built on the same iterator, so it also runs bottom to top. So do forEach, List.copyOf(stack) and anything else that consumes the iterator.

What is the difference between search() and indexOf()?

search counts from the top starting at 1 and returns -1 when absent. indexOf counts from the bottom starting at 0 and returns -1. They give different numbers for the same element on the same stack.

Is Stack thread-safe?

Every individual method is synchronised, which is not the same thing. Any sequence of two calls — empty() then pop(), peek() then push() — can interleave. Compound operations need an external lock regardless.

Which is faster, Stack or ArrayDeque?

ArrayDeque, mostly because of the uncontended lock Stack takes on every call. The gap is small enough not to matter in code that does anything else, and large enough to show up in a tight loop.

Can ArrayDeque hold null?

No — it throws NullPointerException on insert. That is deliberate: peek() and poll() use null to signal an empty deque, which only works if null cannot be a real element.

How do I make an ArrayDeque thread-safe?

ConcurrentLinkedDeque for lock-free access, LinkedBlockingDeque when you also want a bound and blocking takes, or your own lock around the compound operations. Wrapping it in Collections.synchronizedCollection gives you Stack’s problem back.

Why does pop() throw instead of returning null?

Stack.pop throws EmptyStackException and ArrayDeque.pop throws NoSuchElementException. Use pollFirst() on a Deque when you want the null-returning version and the drain loop it enables.

Should I use Stack for a LeetCode-style problem?

Use Deque<Integer> stack = new ArrayDeque<>(). Same methods, correct iteration order if you print it while debugging, and no boxing surprises beyond the ones Integer already gives you.

Is there a stack in the Java standard library that is immutable?

Not as such. List.of(...) with index arithmetic is the usual substitute, or a persistent-collections library if you want structural sharing.