Stack Data Structure — Unguarded pop() Crashes Demo
EmptyStackException crashed a live demo when user undid past history.
- Core operations: push (add to top), pop (remove from top), peek (read top). All O(1).
- Backed by an array with a single topIndex pointer. Push increments it, pop decrements it.
- In Java, use Deque
stack = new ArrayDeque<>() — not java.util.Stack. - Stack Overflow: pushing to a full stack. Stack Underflow: popping from empty. Both throw exceptions.
- Production risk: unguarded pop() on an empty stack causes EmptyStackException. Always check isEmpty() first.
- Real-world: undo/redo, call stack, expression parsing, DFS, bracket matching.
Imagine a stack of dinner plates at a buffet. You can only add a new plate on top, and you can only grab the plate from the top — you'd never yank one from the middle. That's exactly what a stack data structure is: a collection where the last item you put in is always the first item you take out. Computer scientists call this LIFO — Last In, First Out. Your browser's back button works this way: every page you visit gets 'stacked', and hitting back peels the top one off.
A Stack is a LIFO data structure that restricts access to a single endpoint — the top. This restriction is the feature: it prevents accidental ordering bugs that a free-form list would allow.
Every programming language uses a call stack to track function execution. Every text editor uses a stack for undo/redo. Every compiler uses a stack for expression parsing and bracket matching. These are not academic exercises — they are the invisible scaffolding of production software.
A common misconception is that java.util.Stack is the correct choice in Java. It is not. Stack extends Vector, inherits synchronization overhead, and exposes random-access methods that break the LIFO contract. The Java documentation recommends ArrayDeque as the modern replacement.
What Is a Stack and Why Does the LIFO Rule Matter?
A stack is a linear data structure that follows one strict rule: the last element added is the first element removed. This rule has a name — LIFO, which stands for Last In, First Out. Every single operation on a stack flows from this one rule, so if you understand LIFO deeply, everything else will make sense automatically.
Think about a browser's back button again. You open Google (page 1), click a link to Wikipedia (page 2), then click another link to a Wikipedia article (page 3). Now you hit back. You go to page 2 — not page 1. Hit back again, you get to page 1. The browser didn't remember pages in alphabetical order or by size — it remembered them in the order you visited, and it unwinds them in reverse. That's LIFO.
A stack only exposes three core operations to you. Push adds an item to the top. Pop removes the item from the top. Peek (sometimes called top) lets you look at the top item without removing it. That's the entire interface. The power of a stack doesn't come from complexity — it comes from constraint. By refusing to let you touch anything except the top, it forces you into a pattern that maps perfectly onto a whole family of real problems.
- Undo: the last action taken should be the first action reversed.
- Function calls: the most recently called function should return first.
- Bracket matching: the most recently opened bracket must close next.
- DFS: the most recently discovered node should be explored first.
How a Stack Works — Internals, Operations, and the O(1) Guarantee
A stack is a Last-In, First-Out (LIFO) data structure — the most recently added item is the first to be removed, like a stack of plates: you add to the top and remove from the top.
Core operations (all O(1)): 1. push(x): add x to the top. 2. pop(): remove and return the top element. Error if empty. 3. peek() / top(): return the top element without removing it. 4. is_empty(): return True if the stack has no elements.
Worked example — process '[({})[]{}]' with a stack to check balanced parentheses: '[': push. Stack: ['['] '(': push. Stack: ['[','('] '{': push. Stack: ['[','(','{'] '}': pop '{', matches. Stack: ['[','('] ')': pop '(', matches. Stack: ['['] '[': push. Stack: ['[','['] ']': pop '[', matches. Stack: ['['] '{': push. Stack: ['[','{'] '}': pop '{', matches. Stack: ['['] ']': pop '[', matches. Stack: [] Stack empty at end. Balanced!
- push: increment topIndex, write to array[topIndex]. Two operations, constant time.
- pop: read array[topIndex], decrement topIndex. Two operations, constant time.
- peek: read array[topIndex]. One operation, constant time.
- No element shifting (unlike list.pop(0) in Python or ArrayList.remove(0) in Java).
Building a Stack From Scratch (So You Actually Understand It)
Java's built-in Stack class is fine, but building one yourself from scratch is the single best way to truly understand what's happening under the hood. Once you've done it, you'll never be confused by stack internals again.
Under the hood, a stack is almost always built on top of an array or a linked list. We'll use an array here because arrays are the most visual — you can picture the slots filling up one by one. The key insight is that we maintain a variable called topIndex that always points to the position of the most recently added element. Every push moves topIndex up by one. Every pop moves it down by one. Peek just reads whatever is at topIndex without moving it.
We also need to handle two failure cases. If you pop from an empty stack, there's nothing to remove — that's called an underflow. If you push to a full stack (when using a fixed-size array), there's no room — that's called an overflow. Production-grade stacks handle both gracefully. Our implementation will throw clear, descriptive exceptions so you know exactly what went wrong.
- Pop decrements topIndex but does not zero out elements[topIndex + 1].
- The old value remains in memory but is unreachable — the stack treats it as deleted.
- This avoids a memory write on every pop, keeping the operation at two instructions.
- For garbage-collected languages, this is harmless. For manual memory management, it could be a leak.
Worked Example — Stack Operations Step by Step (Postfix Evaluation & Bracket Matching)
Simulate evaluating a postfix expression '2 3 4 * +' using a stack:
- Read '2': push 2. Stack: [2].
- Read '3': push 3. Stack: [2, 3].
- Read '4': push 4. Stack: [2, 3, 4].
- Read '': pop 4 and 3, compute 34=12, push 12. Stack: [2, 12].
- Read '+': pop 12 and 2, compute 2+12=14, push 14. Stack: [14].
- End of expression: pop result 14. Answer: 14.
Balanced parentheses check for '{[()]}': push '{', push '[', push '('. See ')': pop '(', match. See ']': pop '[', match. See '}': pop '{', match. Stack empty → balanced.
For '([)]': push '(', push '['. See ')': top is '[', mismatch → unbalanced.
All stack operations (push, pop, peek) are O(1) because we only touch the top element.
- Infix (3 + 4) requires precedence rules and parentheses. Postfix (3 4 +) does not.
- The stack's LIFO order naturally separates operands from operators.
- When you see an operator, the two most recent values on the stack are its operands.
- This is exactly what calculators and compilers use internally.
peek() but forgot to pop() after using the operator — the stack kept growing, causing a memory leak. Always pair push with a matching pop."A Real Problem Stacks Solve — Checking Balanced Brackets
Here's the moment where stacks go from interesting theory to genuinely useful tool. One of the most classic stack problems — and one interviewers love — is checking whether brackets in an expression are balanced.
Consider the expression: {[()]}. Every opening bracket has a matching closing bracket in the right order. That's balanced. Now consider {[(])} — the square bracket closes before the round bracket does. That's not balanced, and any compiler would reject it.
Here's why a stack is the perfect tool: every time you see an opening bracket, you push it. Every time you see a closing bracket, you peek at the top of the stack. If the top is the matching opener, pop it and continue. If it isn't, the expression is invalid. If you reach the end and the stack is empty, every opener was matched. This algorithm works because the most recently opened bracket must always be the next one closed — which is exactly the LIFO pattern.
This same logic powers syntax highlighting in every IDE, HTML/XML validators, and expression evaluators in calculators and compilers.
- When you see a closing bracket, the matching opener is always the most recently opened one.
- A Stack naturally gives you the most recently added item — that is exactly what you need.
- A Queue would give you the earliest opener, which is wrong for nested structures.
- This is why LIFO is not just a preference — it is the algorithmic requirement.
Java's Built-In Stack vs Deque — Which One Should You Actually Use?
Java has a built-in Stack class in java.util. It works fine and it's easy to use — but there's a catch that trips up a lot of developers, especially in interviews.
Stack in Java extends Vector, which is a legacy class from Java 1.0. This means it's synchronized (thread-safe) by default, which sounds nice but adds unnecessary overhead in single-threaded applications. It also inherits all of Vector's methods, which means you can call things like stack.get(2) or stack.remove(0) — operations that completely bypass LIFO and break the whole point of using a stack.
The Java documentation itself recommends using ArrayDeque when you need a stack. ArrayDeque implements the Deque interface and is faster, more memory-efficient, and doesn't expose non-stack methods in the typical usage pattern. You use , push(), and pop() exactly as you would with peek()Stack, but you get better performance under the hood.
The comparison table below breaks down the key differences so you can make the right choice for any situation.
- Stack extends Vector — it inherits synchronization overhead on every operation.
- Vector exposes get(index), remove(index), add(index, element) — these bypass LIFO entirely.
- In a single-threaded context, synchronization adds ~2x overhead with zero benefit.
- ArrayDeque is the official Java recommendation. Use Deque<Type> stack = new ArrayDeque<>().
The Undo System That Crashed During a Live Demo
pop() call. The crash was 100% reproducible when the user undid more actions than existed in the history.stack.pop() without checking isEmpty() first. When the user pressed Ctrl+Z on an empty undo history, pop() threw EmptyStackException. The exception was not caught, propagated to the event loop, and crashed the application. The fix was trivial — one isEmpty() check — but the bug had existed for 18 months because no one had ever tried to undo past the beginning of history in normal usage.pop() and peek() call in the undo handler.
2. Replaced java.util.Stack with ArrayDeque — the recommended Java implementation.
3. Added a unit test that performs 1,000 undo operations on an empty stack and verifies no exception is thrown.
4. Added a defensive catch block around the undo handler that logs a warning instead of crashing.- Unguarded
pop()on an empty stack is the most common Stack bug. Always check isEmpty() first. - Edge cases that never occur in normal usage are the most dangerous — they survive testing and crash in demos.
- java.util.Stack should not be used in new code. Use ArrayDeque as the Java documentation recommends.
- Defensive programming (isEmpty() check + catch block) is not over-engineering — it is the difference between a warning log and a crashed application.
pop() and peek() call. Replace bare stack.pop() with a guarded method that returns Optional or a default value.push() and reject or evict when the limit is reached.pop() in isEmpty() check. Replace bare pop() with safePop() that returns Optional.Key takeaways
pop() and peek() with an isEmpty() check. A single unguarded pop() on an empty stack throws an unchecked exception and crashes your programCommon mistakes to avoid
6 patternsPopping from an empty stack
pop() or peek(). Get in the habit of writing 'if (!stack.isEmpty()) { stack.pop(); }' — never call pop() blindly.Using java.util.Stack and calling get() or elementAt()
Confusing push direction in ArrayDeque
push(), pop(), and peek(). Never mix in offer(), poll(), or add() — those are queue operations that work from the other end and will destroy your ordering.Not handling stack overflow in custom implementations
Using java.util.Stack in performance-critical code
Forgetting to check isEmpty() after a series of pops
size() before popping in a loop, or guard each pop with isEmpty(). Prefer the guard pattern — it handles all cases.Interview Questions on This Topic
What is the difference between a stack and a queue, and can you give a real-world example of each?
Frequently Asked Questions
That's Stack & Queue. Mark it forged?
5 min read · try the examples if you haven't