Java Tutorial
🔍

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

Operationjava.util.StackArrayDeque (as Deque)Notes
push(element)O(1) amort + lockO(1) amortLock per call in Stack
pop()O(1) + lockO(1)Lock per call in Stack
peek()O(1) + lockO(1)Lock per call in Stack
empty()O(1) + lockO(1) (isEmpty())
search(object)O(n) + lockN/A (not in Deque)Linear scan — no equivalent
Memory growth×2 (doubled)×2 (doubled)Both circular/resizable arrays
EncapsulationLeaks List methodsOnly Deque methodsDeque is the correct contract
Thread safetyGlobal lockNot thread-safeUse 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 add

Mistake 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