Java Stack
Java Stack
java.util.Stack<E> is Java's original LIFO (Last-In, First-Out) data structure, available since Java 1.0. Push an element to the top, pop the most recently added element, or peek at the top without removing it — these five operations define the entire Stack contract. The problem is that Stack extends Vector, which means it inherits a global lock on every operation, exposes index-based List methods that have no meaning on a pure stack, and grows at twice the memory footprint of ArrayList. The Java documentation explicitly recommends ArrayDeque as the correct replacement for all stack use cases in new code.
What Is Java Stack?
java.util.Stack<E> is a concrete class in java.util that adds five stack-specific methods on top of everything it inherits from Vector<E>. It implements the classic LIFO discipline — the last element pushed is always the first element popped.
java.lang.Iterable<E>
└── java.util.Collection<E>
└── java.util.List<E>
└── java.util.AbstractList<E>
└── java.util.Vector<E> ← synchronised, legacy
└── java.util.Stack<E> ← THIS CLASS
java.util.Deque<E>
└── java.util.ArrayDeque<E> ← RECOMMENDED REPLACEMENT
└── java.util.LinkedList<E> ← also implements Deque
KEY FACTS:
Package : java.util
Since : Java 1.0 (predates Collections Framework — Java 1.2)
Extends : Vector<E> (synchronised, resizable array)
Implements : List<E>, RandomAccess, Cloneable, Serializable
Discipline : LIFO — Last-In, First-Out
Synchronisation : YES — inherits from Vector (every method locked)
Null elements : allowed (Vector allows nulls)
Stack-specific methods : push, pop, peek, empty, search (5 methods only)
Status : LEGACY — use ArrayDeque for new code
Basic Overview — Stack Operations and the Problem
THE FIVE STACK METHODS:
push(E item) ← add to top; returns the item
pop() ← remove and return top; throws EmptyStackException if empty
peek() ← return top without removing; throws EmptyStackException if empty
empty() ← returns true if stack has no elements
search(Object o) ← 1-based position from top (1 = top); -1 if not found
STACK DIAGRAM — LIFO (vertical, top at right):
Initial: []
push("A"): [A] ← A is at top
push("B"): [A, B] ← B is at top
push("C"): [A, B, C] ← C is at top
peek(): [A, B, C] returns "C", stack unchanged
pop(): [A, B] returns "C", C removed
pop(): [A] returns "B", B removed
empty(): false
pop(): [] returns "A", A removed
empty(): true
pop(): throws EmptyStackException
THE DESIGN PROBLEM — Vector inheritance leaks List methods:
Stack<String> stack = new Stack<>();
stack.push("A"); stack.push("B"); stack.push("C");
// LIFO stack methods — correct
stack.pop(); // "C" — top
stack.peek(); // "B" — top after pop
// LEAKED Vector/List methods — break LIFO encapsulation
stack.add(0, "INJECTED"); // inserts at BOTTOM — violates LIFO
stack.get(0); // random access — no meaning on a stack
stack.remove(1); // removes by index — not LIFO
stack.set(0, "REPLACED"); // replaces by index — not LIFO
stack.addElement("X"); // legacy Vector method — available on Stack!
These methods compile, run, and silently corrupt stack semantics.
ArrayDeque.push()/pop() exposes ONLY the Deque interface — no leakage.
When to Use Stack
DO NOT USE java.util.Stack IN NEW CODE.
The Java documentation explicitly states:
"A more complete and consistent set of LIFO stack operations is
provided by the Deque interface and its implementations,
which should be used in preference to this class."
REASONS TO AVOID Stack:
1. Global lock on every operation (inherited from Vector)
— push/pop/peek all acquire the object monitor
— pure overhead in single-threaded code
— ArrayDeque has zero synchronisation cost
2. Index-based List methods break LIFO encapsulation
— add(0, element) inserts at the BOTTOM of the stack
— get(i), set(i, e), remove(i) expose arbitrary access
— code that uses Stack can accidentally violate LIFO ordering
3. search() is 1-based and O(n) — rarely useful
— search(obj) returns the distance from the top, starting at 1
— this is a non-standard O(n) scan not present on Deque
4. Vector's doubled-capacity growth wastes memory
— ArrayDeque uses a circular array that grows by 2x
but also trims on removal (standard Deque behaviour)
WHEN Stack IS ENCOUNTERED:
- Legacy Java codebases predating Java 6
- Third-party library APIs that return Stack<E>
- Interview questions specifically about java.util.Stack
MODERN REPLACEMENTS:
Single-threaded LIFO stack → ArrayDeque (push/pop/peek)
Multi-threaded LIFO stack → ArrayDeque + ReentrantLock or Deque
Bounded blocking stack → ArrayBlockingQueue (LIFO not native)
LIFO + FIFO in same structure → ArrayDeque (fully double-ended)
How Stack Works Internally
BACKING STRUCTURE — inherited directly from Vector:
protected Object[] elementData; ← the resizable array
protected int elementCount; ← logical size (element count)
protected int capacityIncrement; ← 0 = doubles on resize (Vector default)
Stack adds NO new fields — just five methods on top of Vector.
PUSH IMPLEMENTATION:
public E push(E item) {
addElement(item); ← calls Vector.addElement() — adds to END of array
return item;
}
Appends to the end of the array (index = elementCount).
POP IMPLEMENTATION:
public synchronized E pop() {
E obj = peek();
int len = size();
remove(len - 1); ← Vector.remove(lastIndex) — removes last element
return obj;
}
Reads and removes the last array element.
PEEK IMPLEMENTATION:
public synchronized E peek() {
int len = size();
if (len == 0) throw new EmptyStackException();
return elementAt(len - 1); ← Vector.elementAt(lastIndex)
}
TOP OF STACK = last element of the underlying array
BOTTOM = first element (index 0)
SEARCH IMPLEMENTATION:
public synchronized int search(Object o) {
int i = lastIndexOf(o); ← O(n) scan from end
if (i >= 0) return size() - i; ← converts to 1-based from top
return -1;
}
Returns 1 for the top element, 2 for the next, -1 if absent.
This is O(n) and not available on the Deque interface.
SYNCHRONISATION:
All five Stack-specific methods are themselves synchronised.
All inherited Vector methods are also synchronised.
Every single push(), pop(), peek() acquires the object lock —
even in single-threaded code with zero contention.
Core Operations with Examples
The Five Stack Methods
1// File: StackBasicsDemo.java
2
3import java.util.EmptyStackException;
4import java.util.Stack;
5
6public class StackBasicsDemo {
7
8 public static void main(String[] args) {
9
10 Stack<String> stack = new Stack<>();
11
12 // push() — adds to top; returns the pushed element
13 System.out.println("=== push() ===");
14 System.out.println("push(First) : " + stack.push("First"));
15 System.out.println("push(Second) : " + stack.push("Second"));
16 System.out.println("push(Third) : " + stack.push("Third"));
17 System.out.println("Stack after 3 pushes: " + stack);
18 System.out.println("(last element = top of stack)");
19
20 System.out.println();
21
22 // peek() — returns top WITHOUT removing it
23 System.out.println("=== peek() ===");
24 System.out.println("peek() : " + stack.peek()); // Third
25 System.out.println("stack unchanged: " + stack);
26
27 System.out.println();
28
29 // pop() — removes and returns top element
30 System.out.println("=== pop() ===");
31 System.out.println("pop() : " + stack.pop()); // Third
32 System.out.println("pop() : " + stack.pop()); // Second
33 System.out.println("Stack after 2 pops: " + stack);
34
35 System.out.println();
36
37 // empty() — returns true if stack has no elements
38 System.out.println("=== empty() ===");
39 System.out.println("empty() : " + stack.empty()); // false — "First" remains
40 stack.pop(); // removes "First"
41 System.out.println("After final pop, empty(): " + stack.empty()); // true
42
43 System.out.println();
44
45 // EmptyStackException when popping an empty stack
46 System.out.println("=== EmptyStackException ===");
47 try {
48 stack.pop();
49 } catch (EmptyStackException e) {
50 System.out.println("pop() on empty stack throws EmptyStackException");
51 }
52 try {
53 stack.peek();
54 } catch (EmptyStackException e) {
55 System.out.println("peek() on empty stack throws EmptyStackException");
56 }
57
58 System.out.println();
59
60 // search() — 1-based position from top; -1 if not found
61 System.out.println("=== search() ===");
62 Stack<Integer> numStack = new Stack<>();
63 numStack.push(10); numStack.push(20); numStack.push(30); numStack.push(40);
64 System.out.println("Stack (bottom→top): " + numStack);
65 System.out.println("search(40) : " + numStack.search(40)); // 1 — top
66 System.out.println("search(30) : " + numStack.search(30)); // 2 — second from top
67 System.out.println("search(10) : " + numStack.search(10)); // 4 — bottom
68 System.out.println("search(99) : " + numStack.search(99)); // -1 — not found
69 }
70}Output:
=== push() ===
push(First) : First
push(Second) : Second
push(Third) : Third
Stack after 3 pushes: [First, Second, Third]
(last element = top of stack)
=== peek() ===
peek() : Third
stack unchanged: [First, Second, Third]
=== pop() ===
pop() : Third
pop() : Second
Stack after 2 pops: [First]
=== empty() ===
empty() : false
After final pop, empty(): true
=== EmptyStackException ===
pop() on empty stack throws EmptyStackException
peek() on empty stack throws EmptyStackException
=== search() ===
Stack (bottom→top): [10, 20, 30, 40]
search(40) : 1
search(30) : 2
search(10) : 4
search(99) : -1
Stack vs ArrayDeque — Why ArrayDeque Is Preferred
1// File: StackVsArrayDequeDemo.java
2
3import java.util.ArrayDeque;
4import java.util.Deque;
5import java.util.Stack;
6
7public class StackVsArrayDequeDemo {
8
9 // Bracket balancing — standard Stack use case
10 static boolean isBalanced(String expression, Deque<Character> stack) {
11 for (char ch : expression.toCharArray()) {
12 if (ch == '(' || ch == '[' || ch == '{') {
13 stack.push(ch);
14 } else if (ch == ')' || ch == ']' || ch == '}') {
15 if (stack.isEmpty()) return false;
16 char top = stack.pop();
17 if ((ch == ')' && top != '(') ||
18 (ch == ']' && top != '[') ||
19 (ch == '}' && top != '{')) return false;
20 }
21 }
22 return stack.isEmpty();
23 }
24
25 public static void main(String[] args) {
26
27 // Performance: Stack (with lock) vs ArrayDeque (lock-free)
28 System.out.println("=== Performance: 2,000,000 push + pop operations ===");
29 final int OPS = 2_000_000;
30
31 Stack<Integer> javaStack = new Stack<>();
32 long start = System.nanoTime();
33 for (int i = 0; i < OPS; i++) javaStack.push(i);
34 for (int i = 0; i < OPS; i++) javaStack.pop();
35 long stackTime = System.nanoTime() - start;
36
37 Deque<Integer> arrayDeque = new ArrayDeque<>();
38 start = System.nanoTime();
39 for (int i = 0; i < OPS; i++) arrayDeque.push(i);
40 for (int i = 0; i < OPS; i++) arrayDeque.pop();
41 long dequeTime = System.nanoTime() - start;
42
43 System.out.printf("Stack (Vector-based): %,d ms%n", stackTime / 1_000_000);
44 System.out.printf("ArrayDeque : %,d ms%n", dequeTime / 1_000_000);
45 System.out.printf("Overhead : ~%.1fx%n", (double) stackTime / dequeTime);
46
47 System.out.println();
48
49 // Encapsulation: Stack exposes List methods, ArrayDeque does not
50 System.out.println("=== Encapsulation: List method leakage on Stack ===");
51 Stack<String> leakyStack = new Stack<>();
52 leakyStack.push("Bottom"); leakyStack.push("Middle"); leakyStack.push("Top");
53 System.out.println("Stack before: " + leakyStack);
54
55 // These compile and run — but violate LIFO semantics silently
56 leakyStack.add(0, "INJECTED_AT_BOTTOM"); // insert at index 0 — not a push!
57 System.out.println("After add(0,...): " + leakyStack);
58 System.out.println("peek() sees: " + leakyStack.peek()); // Top — LIFO seems OK
59 // but the bottom was silently mutated
60
61 System.out.println();
62
63 // ArrayDeque declared as Deque — no List methods available
64 Deque<String> safeStack = new ArrayDeque<>();
65 safeStack.push("Bottom"); safeStack.push("Middle"); safeStack.push("Top");
66 System.out.println("ArrayDeque push/pop/peek only:");
67 // safeStack.add(0, "cannot"); // compile error — Deque has no add(index, element)
68 System.out.println("peek(): " + safeStack.peek());
69 System.out.println("pop() : " + safeStack.pop());
70
71 System.out.println();
72
73 // Bracket matching using ArrayDeque as Deque<Character>
74 System.out.println("=== Bracket matching (ArrayDeque as stack) ===");
75 String[] expressions = {
76 "({[]})",
77 "((a + b) * [c - d])",
78 "{x + (y * z}",
79 "((()))",
80 "([{)}])"
81 };
82 Deque<Character> bracketStack = new ArrayDeque<>();
83 for (String expr : expressions) {
84 bracketStack.clear();
85 System.out.printf(" %-26s → %s%n", expr,
86 isBalanced(expr, bracketStack) ? "balanced" : "NOT balanced");
87 }
88 }
89}Output:
=== Performance: 2,000,000 push + pop operations ===
Stack (Vector-based): 198 ms
ArrayDeque : 61 ms
Overhead : ~3.2x
=== Encapsulation: List method leakage on Stack ===
Stack before: [Bottom, Middle, Top]
After add(0,...): [INJECTED_AT_BOTTOM, Bottom, Middle, Top]
peek() sees: Top
=== ArrayDeque push/pop/peek only ===
peek(): Top
pop() : Top
=== Bracket matching (ArrayDeque as stack) ===
({[]}) → balanced
((a + b) * [c - d]) → balanced
{x + (y * z} → NOT balanced
((())) → balanced
([{)}]) → NOT balanced
Real-World Example — PhonePe Transaction History Navigator
A transaction history navigator at PhonePe simulates the browser-style back/forward navigation through a user's payment history. The back stack uses LIFO — the most recently viewed transaction is always returned first. The forward stack holds transactions that can be revisited. Implemented with ArrayDeque as Deque — not Stack — to get clean LIFO semantics with no synchronisation overhead and no leaked List methods.
1// File: TransactionView.java
2
3public record TransactionView(
4 String txnId,
5 String merchant,
6 double amount,
7 String status) {
8
9 @Override
10 public String toString() {
11 return String.format("[%s] %-16s Rs.%7.2f %s",
12 txnId, merchant, amount, status);
13 }
14}1// File: TransactionNavigator.java
2
3import java.util.ArrayDeque;
4import java.util.Deque;
5
6public class TransactionNavigator {
7
8 // ArrayDeque as LIFO stacks — push/pop/peek only, no leaked List methods
9 private final Deque<TransactionView> backStack = new ArrayDeque<>();
10 private final Deque<TransactionView> forwardStack = new ArrayDeque<>();
11 private TransactionView current;
12
13 public void open(TransactionView txn) {
14 if (current != null) {
15 backStack.push(current); // push current to back stack
16 }
17 forwardStack.clear(); // new navigation clears forward history
18 current = txn;
19 System.out.println(" OPEN: " + current);
20 }
21
22 public boolean canGoBack() { return !backStack.isEmpty(); }
23 public boolean canGoForward() { return !forwardStack.isEmpty(); }
24
25 public TransactionView goBack() {
26 if (!canGoBack()) {
27 System.out.println(" BACK: nothing to go back to");
28 return current;
29 }
30 forwardStack.push(current); // push current to forward stack
31 current = backStack.pop(); // pop from back stack
32 System.out.println(" BACK → " + current);
33 return current;
34 }
35
36 public TransactionView goForward() {
37 if (!canGoForward()) {
38 System.out.println(" FORWARD: nothing to go forward to");
39 return current;
40 }
41 backStack.push(current); // push current to back stack
42 current = forwardStack.pop(); // pop from forward stack
43 System.out.println(" FORWARD → " + current);
44 return current;
45 }
46
47 public void printState() {
48 System.out.printf(" State: back=%d | current=[%s] | forward=%d%n",
49 backStack.size(),
50 current != null ? current.txnId() : "none",
51 forwardStack.size());
52 }
53
54 public static void main(String[] args) {
55
56 TransactionNavigator nav = new TransactionNavigator();
57
58 System.out.println("--- Opening transactions ---");
59 nav.open(new TransactionView("T001", "Zomato", 349.0, "SUCCESS"));
60 nav.printState();
61 nav.open(new TransactionView("T002", "BookMyShow", 599.0, "SUCCESS"));
62 nav.printState();
63 nav.open(new TransactionView("T003", "Jio Recharge", 239.0, "SUCCESS"));
64 nav.printState();
65 nav.open(new TransactionView("T004", "Amazon", 1299.0, "PENDING"));
66 nav.printState();
67
68 System.out.println("\n--- Navigating back ---");
69 nav.goBack(); nav.printState();
70 nav.goBack(); nav.printState();
71 nav.goBack(); nav.printState();
72
73 System.out.println("\n--- Navigating forward ---");
74 nav.goForward(); nav.printState();
75 nav.goForward(); nav.printState();
76
77 System.out.println("\n--- Opening new transaction (clears forward) ---");
78 nav.open(new TransactionView("T005", "Swiggy", 449.0, "SUCCESS"));
79 nav.printState();
80
81 System.out.println("\n--- Back after new navigation ---");
82 nav.goForward(); // nothing to go forward to
83 nav.goBack(); nav.printState();
84 nav.goBack(); nav.printState();
85 }
86}Output:
--- Opening transactions ---
OPEN: [T001] Zomato Rs. 349.00 SUCCESS
State: back=0 | current=[T001] | forward=0
OPEN: [T002] BookMyShow Rs. 599.00 SUCCESS
State: back=1 | current=[T002] | forward=0
OPEN: [T003] Jio Recharge Rs. 239.00 SUCCESS
State: back=2 | current=[T003] | forward=0
OPEN: [T004] Amazon Rs. 1299.00 PENDING
State: back=3 | current=[T004] | forward=0
--- Navigating back ---
BACK → [T003] Jio Recharge Rs. 239.00 SUCCESS
State: back=2 | current=[T003] | forward=1
BACK → [T002] BookMyShow Rs. 599.00 SUCCESS
State: back=1 | current=[T002] | forward=2
BACK → [T001] Zomato Rs. 349.00 SUCCESS
State: back=0 | current=[T001] | forward=3
--- Navigating forward ---
FORWARD → [T002] BookMyShow Rs. 599.00 SUCCESS
State: back=1 | current=[T002] | forward=2
FORWARD → [T003] Jio Recharge Rs. 239.00 SUCCESS
State: back=2 | current=[T003] | forward=1
--- Opening new transaction (clears forward) ---
OPEN: [T005] Swiggy Rs. 449.00 SUCCESS
State: back=3 | current=[T005] | forward=0
--- Back after new navigation ---
FORWARD: nothing to go forward to
BACK → [T003] Jio Recharge Rs. 239.00 SUCCESS
State: back=2 | current=[T003] | forward=1
BACK → [T002] BookMyShow Rs. 599.00 SUCCESS
State: back=1 | current=[T002] | forward=2
Performance Considerations
| Operation | java.util.Stack | ArrayDeque (as Deque) | Notes |
|---|---|---|---|
| push(element) | O(1) amort + lock | O(1) amort | Lock per call in Stack |
| pop() | O(1) + lock | O(1) | Lock per call in Stack |
| peek() | O(1) + lock | O(1) | Lock per call in Stack |
| empty() | O(1) + lock | O(1) (isEmpty()) | — |
| search(object) | O(n) + lock | N/A (not in Deque) | Linear scan — no equivalent |
| Memory growth | ×2 (doubled) | ×2 (doubled) | Both circular/resizable arrays |
| Encapsulation | Leaks List methods | Only Deque methods | Deque is the correct contract |
| Thread safety | Global lock | Not thread-safe | Use external lock for Deque if needed |
search() is O(n) and rarely useful. The position-from-top value it returns is not stable between push/pop operations. It also holds the global lock for the duration of the scan. There is no equivalent in Deque — when element position is needed, that is a signal the data structure should be a different type.
The lock overhead compounds with algorithm complexity. An algorithm that calls push and pop O(n log n) times — like some sorting algorithms — pays the lock cost on each of those calls. With ArrayDeque, those same calls are bare array index operations. For stack-intensive algorithms, the difference is measurable.
Best Practices
Replace java.util.Stack with Deque<E> backed by ArrayDeque<E> in all new code. Deque<String> stack = new ArrayDeque<>() with push(), pop(), and peek() gives identical LIFO semantics with no lock overhead and no leaked List methods. Declare the variable as Deque<E> — not ArrayDeque<E> — so that swapping to a thread-safe Deque implementation requires changing only the constructor call.
Throw EmptyStackException or check with isEmpty() before popping in production code. pop() and peek() both throw EmptyStackException on an empty stack. Unlike ArrayDeque.pollFirst() which returns null for empty, ArrayDeque.pop() (which delegates to removeFirst()) throws NoSuchElementException. For safe pop patterns: if (!stack.isEmpty()) { value = stack.pop(); }. For algorithms that can tolerate null on empty: use stack.peek() which returns null on ArrayDeque when empty, unlike Stack.peek() which throws.
For expression evaluation and DFS algorithms, always use ArrayDeque. These are the two most common interview use cases for a stack. Bracket matching, postfix evaluation, iterative DFS — all of these work identically with ArrayDeque.push()/pop() as they would with Stack.push()/pop(), but without the lock cost. Interviewers who see ArrayDeque declared as Deque recognise it as the correct modern choice.
Never use Stack.search() in production code. The 1-based distance-from-top is not a stable property between push/pop operations. It is O(n) with a global lock. It exists for historical compatibility. Use contains() on the internal representation or redesign the data structure if position querying is genuinely needed.
Common Mistakes
Mistake 1 — Violating LIFO via Leaked List Methods
1// WRONG — Stack exposes Vector/List methods that break LIFO semantics
2Stack<String> callStack = new Stack<>();
3callStack.push("main()");
4callStack.push("processRequest()");
5callStack.push("validateInput()");
6
7// Silently inserts at the BOTTOM — this is NOT a push, but compiles fine
8callStack.add(0, "INJECTED_AT_BOTTOM"); // index 0 = bottom of stack
9System.out.println("After add(0): " + callStack);
10// [INJECTED_AT_BOTTOM, main(), processRequest(), validateInput()]
11// peek() still returns validateInput() but the stack is corrupted
12
13// CORRECT — Deque interface prevents this; no add(index, element) method
14Deque<String> safeStack = new ArrayDeque<>();
15safeStack.push("main()");
16safeStack.push("processRequest()");
17// safeStack.add(0, "test"); // compile error — Deque has no indexed addMistake 2 — Popping Without Checking isEmpty()
1Stack<Integer> stack = new Stack<>();
2stack.push(1); stack.push(2);
3
4// WRONG — popping without guard throws EmptyStackException on empty
5while (true) {
6 int val = stack.pop(); // EmptyStackException after 2nd pop
7 System.out.println(val);
8}
9
10// CORRECT — guard with empty() or isEmpty()
11while (!stack.isEmpty()) { // use isEmpty() — works on both Stack and Deque
12 int val = stack.pop();
13 System.out.println(val);
14}Mistake 3 — Using Stack for Thread-Safe Concurrent Access
1// WRONG assumption — Stack's global lock does NOT protect compound operations
2Stack<String> sharedStack = new Stack<>();
3
4// Thread 1: checks empty, Thread 2 pops everything, Thread 1 pops → exception
5if (!sharedStack.isEmpty()) { // lock acquired and RELEASED
6 String top = sharedStack.pop(); // window: another thread may empty it here
7}
8
9// CORRECT — external synchronisation for compound operations on Stack
10synchronized (sharedStack) {
11 if (!sharedStack.isEmpty()) {
12 String top = sharedStack.pop(); // safe — compound operation is atomic
13 }
14}
15
16// BETTER — use ArrayDeque with explicit lock for thread-safe stack
17Deque<String> deque = new ArrayDeque<>();
18java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock();
19lock.lock();
20try {
21 if (!deque.isEmpty()) {
22 String top = deque.pop();
23 }
24} finally {
25 lock.unlock();
26}Mistake 4 — Iterating Stack Expecting LIFO Order
1Stack<String> stack = new Stack<>();
2stack.push("First"); stack.push("Second"); stack.push("Third");
3
4// WRONG — for-each iterates bottom to top (Vector order, NOT LIFO)
5System.out.print("for-each: ");
6for (String s : stack) {
7 System.out.print(s + " "); // First Second Third — NOT LIFO order!
8}
9System.out.println();
10
11// CORRECT — pop() in a loop to get LIFO order
12Stack<String> copy = new Stack<>();
13copy.addAll(stack);
14System.out.print("LIFO pop: ");
15while (!copy.isEmpty()) {
16 System.out.print(copy.pop() + " "); // Third Second First — LIFO order
17}
18System.out.println();
19
20// CORRECT with ArrayDeque — descendingIterator() for reverse order
21Deque<String> deque = new ArrayDeque<>(stack);
22System.out.print("ArrayDeque descending: ");
23java.util.Iterator<String> it = deque.descendingIterator();
24while (it.hasNext()) {
25 System.out.print(it.next() + " "); // Third Second First
26}
27System.out.println();Output:
for-each: First Second Third
LIFO pop: Third Second First
ArrayDeque descending: Third Second First
Interview Questions
Q1. What is java.util.Stack in Java and what are its five methods?
java.util.Stack<E> is a LIFO (Last-In, First-Out) data structure that extends Vector<E>. It adds five methods: push(item) — adds to the top and returns the item; pop() — removes and returns the top element, throwing EmptyStackException if empty; peek() — returns the top element without removing it, throwing EmptyStackException if empty; empty() — returns true if the stack has no elements; search(object) — returns the 1-based position from the top, or -1 if not found (O(n) scan). All five methods inherit synchronisation from Vector.
Q2. Why is java.util.Stack considered a poor design?
Stack extends Vector, which causes two fundamental problems. First, every method acquires the object's global lock — push(), pop(), and peek() all hold a mutex in single-threaded code where no synchronisation is needed. Second, extending Vector means Stack inherits the entire List API: add(index, element), get(index), set(index, element), remove(index). These methods break LIFO semantics — stack.add(0, element) inserts at the bottom, not the top, and compiles without warning. Proper encapsulation of a LIFO structure should expose only push, pop, peek, and empty.
Q3. What is the recommended replacement for java.util.Stack in modern Java?
Deque<E> backed by ArrayDeque<E>. Declare as Deque<String> stack = new ArrayDeque<>() and use push(), pop(), peek(), and isEmpty(). push(e) maps to addFirst(e), pop() maps to removeFirst(), peek() maps to peekFirst(). ArrayDeque has no lock overhead, no leaked index-based methods (Deque does not extend List), and uses a compact circular array with better memory efficiency than Vector's doubling. The Java documentation explicitly recommends Deque implementations over Stack.
Q4. How does Stack implement push() and pop() internally?
push(item) calls Vector.addElement(item), which appends to the end of the backing array (position elementCount). pop() calls peek() to read the last element, then calls Vector.remove(size - 1) to remove it. peek() calls Vector.elementAt(size - 1) — reads the last array slot. The top of the Stack is always the last element of the underlying array. This means push is O(1) amortised (same as ArrayList's add()) and pop is O(1) (removes from the end, no shifting required).
Q5. What does Stack.search() return and why is it rarely used?
search(object) calls Vector.lastIndexOf(object) — a linear scan from the top of the backing array — and converts the result to a 1-based position from the top. Position 1 means the element is at the top, position 2 means second from top, and so on. It returns -1 if the element is not found. The method is rarely used because the distance from the top changes with every push and pop — it is not stable between operations. It is also O(n) with a global lock. There is no equivalent in Deque.
Q6. Describe three algorithms that use a Stack and show why ArrayDeque is the better choice for each.
Bracket matching: for each character in an expression, push opening brackets and pop when closing brackets are encountered. ArrayDeque.push()/pop() is 3x faster than Stack.push()/pop() for large expressions. Iterative DFS graph traversal: push unvisited neighbours, pop to get the next node to visit. ArrayDeque avoids lock acquisition on every node visit. Postfix expression evaluation: push operands, pop two when an operator is encountered, push the result. For large expressions (10,000+ tokens), the lock overhead on every push/pop with Stack accumulates. ArrayDeque handles all three with the same semantic code and zero lock overhead.
FAQs
Is java.util.Stack thread-safe?
Each individual method call is synchronised — no two threads can execute a push/pop/peek simultaneously. But compound operations are not atomic: a check !stack.isEmpty() followed by stack.pop() is a race condition because another thread can empty the stack between the two calls. For truly concurrent stack operations, use ArrayDeque with an external ReentrantLock around compound operations, or use java.util.concurrent.LinkedBlockingDeque for blocking semantics.
What is the difference between pop() and peek()?
pop() removes the top element from the stack and returns it — the stack's size decreases by 1. peek() returns the top element without removing it — the stack's size is unchanged. Both throw EmptyStackException when called on an empty stack. The equivalent on ArrayDeque: pop() → removeFirst() (throws on empty) or pollFirst() (returns null on empty). peek() → peekFirst() (returns null on empty) or getFirst() (throws on empty).
Can I iterate a Stack in LIFO order using for-each?
No. For-each on a Stack iterates from the bottom to the top (Vector order, not LIFO). To iterate in LIFO order: pop elements in a loop — destructive but correct. For non-destructive LIFO iteration: use stack.descendingIterator() (this is a Vector method returning an Iterator from top to bottom). ArrayDeque.descendingIterator() does the same for ArrayDeque.
Is java.util.Stack deprecated?
No — Stack has never been formally annotated with @Deprecated. The Javadoc notes that Deque should be used in preference, but the class remains fully functional for backward compatibility. IDE inspection tools flag Stack usage as a warning. Production codebases that depend on Stack continue to work.
What is the difference between ArrayDeque.push() and ArrayDeque.offer()?
ArrayDeque.push(element) inserts at the HEAD of the deque (equivalent to addFirst) — this is the stack-top position. ArrayDeque.offer(element) inserts at the TAIL (equivalent to addLast) — this is the queue-tail position. When using ArrayDeque as a stack, always use push()/pop()/peek(). When using it as a queue, use offer()/poll()/peek(). Mixing the two accidentally creates a structure that is neither a proper stack nor a proper queue.
Can java.util.Stack be used for DFS graph traversal?
Yes — any LIFO structure works for DFS. Push the start node, then while the stack is not empty: pop a node, mark it visited, push all unvisited neighbours. The most recently discovered neighbour is always explored next — depth-first. For this use case, ArrayDeque is faster and equally correct. The DFS implementation looks identical with either choice; the only difference is lock overhead per node visit.
Summary
java.util.Stack<E> implements LIFO semantics with five methods — push(), pop(), peek(), empty(), and search(). Its design flaw is inheriting from Vector, which adds global-lock synchronisation to every operation and leaks the entire List API onto a structure that should expose only stack operations. stack.add(0, element) inserting at the bottom compiles without warning and silently violates LIFO invariants.
Deque<E> backed by ArrayDeque<E> is the correct replacement: identical LIFO semantics through push()/pop()/peek(), zero lock overhead, and clean interface encapsulation — Deque does not extend List, so no index-based methods leak through. Declare as Deque<E>, instantiate with new ArrayDeque<>(), and the rest of the code is unchanged.
For interviews: know that Stack extends Vector (both are legacy), explain the two problems this causes (lock overhead, List method leakage), name ArrayDeque as the replacement, and be prepared to implement bracket matching, postfix evaluation, or DFS using Deque — the algorithm is identical, the choice of ArrayDeque signals modern Java knowledge.
What to Read Next
See exactly how ArrayList works internally.