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
| Method | Effect | On empty |
|---|---|---|
push(e) | adds to the top, returns e | — |
pop() | removes and returns the top | EmptyStackException |
peek() | returns the top without removing | EmptyStackException |
empty() | true when there are no elements | — |
search(o) | 1-based distance from the top | returns -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:
Stack | ArrayDeque | Difference |
|---|---|---|
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.