What is Stack
A Stack is an abstract data structure that operates strictly on the LIFO (Last-In, First-Out) principle: the last element added to the stack is always the first element to be re...
🟢 Junior Level
A Stack is an abstract data structure that operates strictly on the LIFO (Last-In, First-Out) principle: the last element added to the stack is always the first element to be removed.
The Kitchen Plate Analogy
A stack functions identically to a spring-loaded stack of clean plates in a cafeteria: you can only place a new plate onto the very top of the stack, and you can only remove whichever plate is currently sitting on the top.
Core Stack Operations
push(e): Pushes an item onto the top of the stack ($O(1)$).pop(): Removes and returns the item from the top of the stack ($O(1)$).peek(): Inspects the top item without removing it ($O(1)$).isEmpty(): Checks whether the stack contains any elements ($O(1)$).
// ❌ OBSOLETE ANTI-PATTERN: Legacy java.util.Stack class
Stack<String> stack = new Stack<>();
// ✅ MODERN PRODUCTION STANDARD: Deque implemented by ArrayDeque
Deque<String> stack = new ArrayDeque<>();
stack.push("Bottom");
stack.push("Middle");
stack.push("Top");
System.out.println(stack.peek()); // "Top" (inspects without removing)
System.out.println(stack.pop()); // "Top" (removes and returns)
System.out.println(stack.pop()); // "Middle"
System.out.println(stack.pop()); // "Bottom"
🟡 Middle Level
1. Architectural Defects of java.util.Stack
Introduced in JDK 1.0, java.util.Stack is widely recognized as one of the most prominent architectural design flaws in the early Java API:
// Declaration in java.util:
public class Stack<E> extends Vector<E>
- Liskov Substitution Principle (LSP) Violation:
By directly extending
Vector,Stackinherits all indexed array manipulation methods:Stack<Integer> stack = new Stack<>(); stack.push(10); stack.push(20); stack.add(0, 999); // ❌ Breaks LIFO! Injects item at the bottom of the stack! stack.remove(1); // ❌ Removes an item from the middle of the stack! stack.get(0); // ❌ Random access read!This breaks LIFO encapsulation: client code can arbitrarily bypass the stack top.
- Synchronization Overhead:
Inheriting from
Vectorforces every method (push,pop,peek) to acquire the instance’s intrinsic monitor lock (synchronized (this)). In modern single-threaded algorithms (parsers, search algorithms), this wastes significant CPU cycles acquiring and releasing uncontended locks. - Peculiar Legacy API:
The
search(Object o)method returns the 1-based distance from the top of the stack down to the element, departing from Java’s universal 0-indexed convention and causing off-by-one bugs.
2. The Official Standard: Deque and ArrayDeque
The official OpenJDK JavaDoc for java.util.Stack includes an explicit architectural warning:
“A more complete and consistent set of LIFO stack operations is provided by the
Dequeinterface and its implementations, which should be used in preference to this class:Deque<T> stack = new ArrayDeque<T>();”
| Dimension | java.util.Stack |
ArrayDeque (Modern Standard) |
LinkedList |
|---|---|---|---|
| Underlying Data Structure | Synchronized dynamic array | Resizable circular array | Doubly-linked node list |
Complexity (push/pop) |
$O(1)$ amortized | $O(1)$ amortized | $O(1)$ strict |
| Thread Synchronization | Synchronized (Slow) | Unsynchronized (Fast) | Unsynchronized |
| LIFO Invariant Guarantee | ❌ Violated (allows get(i)) |
✅ Enforced via Deque |
✅ Enforced via Deque |
| Memory Footprint | Moderate | Lowest (Compact) | High ($24–32\text{ bytes/node}$) |
Empty pop() Exception |
EmptyStackException |
NoSuchElementException |
NoSuchElementException |
🔴 Senior Level
1. High-Performance Stack Mechanics in ArrayDeque
ArrayDeque implements the stack using a circular array buffer whose capacity is always constrained to an exact power of two ($2^k$). This allows the JVM to replace slow modulo arithmetic with an instantaneous bitwise mask:
// How ArrayDeque.push() (addFirst) executes internally:
public void addFirst(E e) {
if (e == null) throw new NullPointerException();
// Decrement head pointer modulo buffer length via single bitwise AND:
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity(); // Doubles buffer size when pointers meet
}
- The operation
(head - 1) & (elements.length - 1)executes in a single CPU clock cycle. - Zero object allocations occur during push operations.
- The backing array resides contiguously in physical RAM, leveraging 64-byte L1/L2 CPU cache lines.
2. Lock-Free Concurrent Stacks: The Treiber Stack Algorithm
When a thread-safe stack is required in high-throughput concurrent systems, wrapping an ArrayDeque in Collections.synchronizedList() creates a catastrophic lock contention bottleneck.
In concurrent systems programming, non-blocking stacks are implemented via Treiber’s Algorithm using atomic CAS (Compare-And-Swap) operations:
public class TreiberStack<E> {
private static class Node<E> {
final E item;
Node<E> next;
Node(E item) { this.item = item; }
}
private final AtomicReference<Node<E>> top = new AtomicReference<>();
public void push(E item) {
Node<E> newHead = new Node<>(item);
Node<E> oldHead;
do {
oldHead = top.get();
newHead.next = oldHead;
} while (!top.compareAndSet(oldHead, newHead)); // Atomic CAS retry loop
}
public E pop() {
Node<E> oldHead;
Node<E> newHead;
do {
oldHead = top.get();
if (oldHead == null) return null;
newHead = oldHead.next;
} while (!top.compareAndSet(oldHead, newHead)); // Lock-free pop
return oldHead.item;
}
}
Alternatively, modern production architectures leverage ConcurrentLinkedDeque for ready-to-use lock-free concurrent LIFO pipelines.
3. Classic Production Use Cases
- Syntax Parsing & Expression Evaluation: Evaluating mathematical expressions, parsing JSON/XML AST trees, and validating balanced delimiters
()[]{}. - Undo/Redo Buffers: Storing discrete state-mutation commands in desktop and web document editors.
- Iterative Depth-First Search (DFS): Managing graph traversal frontiers using an explicit heap stack, preventing runtime
StackOverflowErrorexceptions caused by deep JVM call-stack recursion.
4 Tricky Questions
1. Why does java.util.Stack’s inheritance from Vector fundamentally violate the Liskov Substitution Principle (LSP)?
Answer: The Liskov Substitution Principle states that subtypes must preserve the behavioral contracts and invariants of their parent types without exposing contradictory capabilities.
By defining Stack<E> extends Vector<E>, Stack declared that it “is-a” dynamic array. As a result, Stack inherits all public indexed mutation methods from Vector:
stack.add(index, element)stack.remove(index)stack.insertElementAt(element, index)
A mathematical stack strictly defines access exclusively through its top element (LIFO). Inheriting from Vector allows client code to insert elements into the middle, retrieve elements by index, and remove items from the bottom, completely compromising the LIFO abstraction. Stacks should always be constructed via composition (wrapping an internal buffer) or through a targeted interface (like Deque), rather than inheritance from a list.
2. What is the difference in exception-handling behavior when popping an empty stack in java.util.Stack versus ArrayDeque?
Answer:
- Calling
pop()on an emptyjava.util.Stackthrowsjava.util.EmptyStackException, an unchecked subclass ofRuntimeExceptionspecific to that legacy class. - Calling
pop()(orremoveFirst()) on an emptyArrayDequethrows the standard collection exceptionjava.util.NoSuchElementException. - Furthermore,
ArrayDequeprovides the non-throwing methodpoll()(orpollFirst()), which safely returnsnullwhen invoked on an empty stack, allowing developers to avoid the CPU overhead of exception handling in high-throughput loops.
3. Why does ArrayDeque vastly outperform LinkedList when utilized as a LIFO stack?
Answer:
- Zero GC Allocation Churn: In
LinkedList, everypush()allocates an individualNodeobject on the heap, populating object headers and pointer fields, which must later be garbage-collected when popped.ArrayDequepre-allocates a circular array buffer, executingpush()andpop()as index updates with zero object allocations. - Hardware Cache Locality:
ArrayDequestores element references in a contiguous array in RAM. Reading the top of the stack loads adjacent elements into the CPU’s 64-byte L1/L2 cache lines.LinkedListscatters nodes across the heap, triggering CPU cache misses and pipeline stalls on every traversal. - Memory Footprint:
ArrayDequeconsumes only 4 bytes per pointer under Compressed OOPs, whereasLinkedListconsumes 24 to 32 bytes per node.
4. What is a Treiber Stack, and how does it achieve lock-free thread-safety without synchronized locks?
Answer:
A Treiber Stack is a classic lock-free concurrent algorithm developed by R. Kent Treiber. It represents the stack as a singly-linked list of nodes where the head pointer (top) is managed by an AtomicReference.
- Lock-Free
push(item): The thread creates a new node, reads the currenttop, setsnewNode.next = currentTop, and attempts to updatetopusingcompareAndSet(currentTop, newNode). If another thread updatedtopin the interim, the CAS fails, and the loop retries. - Lock-Free
pop(): The thread readscurrentTop, extractsnext = currentTop.next, and attemptscompareAndSet(currentTop, next). Because it uses hardware atomic instructions (such asLOCK CMPXCHGon x86) instead of OS-level mutexes, threads never block or context-switch, providing superior throughput under concurrent load.
🎯 Interview Cheat Sheet
30-Second Summary
“A Stack is a LIFO (Last-In, First-Out) data structure with primary operations
push(),pop(), andpeek(). In Java, the legacyjava.util.Stackclass is an architectural anti-pattern because it extendsVector(violating LSP by exposing indexed array mutations) and imposes redundant per-method synchronization. In modern Java, always declare stacks using theDequeinterface backed byArrayDeque(Deque<E> stack = new ArrayDeque<>()). It provides amortized $O(1)$ operations with zero node allocations and superior CPU cache locality. For concurrent lock-free stacks, useConcurrentLinkedDequeor a Treiber Stack.”
Common Follow-up Questions
- “Why should you not use LinkedList as a stack?” → It creates a
Nodeobject on everypush(), wasting memory ($8\times$ overhead) and causing CPU cache misses and GC churn. - “How does ArrayDeque wrap around its circular array?” → It constrains its capacity to powers of two and uses bitwise masking:
(head - 1) & (elements.length - 1). - “Can ArrayDeque store null elements?” → No,
ArrayDequestrictly prohibitsnullelements (throwsNullPointerException) because methods likepoll()usenullas a sentinel value indicating that the collection is empty.
Red Flags (DO NOT Say)
- ❌ “I use java.util.Stack in new projects because it is the standard stack class.” (It is obsolete;
ArrayDequeis the official modern recommendation). - ❌ “A Stack allows fast random retrieval of elements by index.” (A true stack only permits access to the top element;
Stack.get(i)is an inheritance defect). - ❌ “ArrayDeque.pop() returns null if the stack is empty.” (
pop()throwsNoSuchElementException; usepollFirst()orpoll()to getnull).
Related Topics
- What is Vector and how does it differ from ArrayList — Vector inheritance flaws
- What is Deque — Deep dive into
ArrayDequecircular buffer - What is Queue and what implementations exist — FIFO vs LIFO contracts
- What is the difference between ArrayList and LinkedList — Cache locality mechanics