Implementations
Three ways to implement a stack in Java, and why the legacy java.util.Stack class is usually avoided: an array, a linked list and ArrayDeque.
Learning goals
- Implement a stack on an array and on a linked list
- Use ArrayDeque as the standard stack
- Explain what is wrong with java.util.Stack
Prerequisites
Array stack
An array stack keeps an int[] and a size: the top is data[size - 1]. Push writes at size and increments it; pop decrements it. Growing by doubling keeps push amortised O(1). Memory is compact and cache friendly.
Step through pushes and pops in the animation to see the array and size.
1class ArrayStack {2private int[] data = new int[4];3private int size;4void push(int x) {5if (size == data.length) data = Arrays.copyOf(data, size * 2);6data[size++] = x;7}8int pop() {9if (size == 0) throw new NoSuchElementException("stack underflow");10return data[--size];11}12int peek() {13if (size == 0) throw new NoSuchElementException("stack underflow");14return data[size - 1];15}16boolean isEmpty() { return size == 0; }17}
Array-backed stack: push, pop, peek, overflow and underflow
Interactive · edit the inputdata[] · capacity 3 · size 0
An empty stack backed by an array of 3. The top is data[size - 1]; push and pop only touch the end, so both are O(1).
1class ArrayStack {2private int[] data; private int size;3ArrayStack(int capacity) { data = new int[capacity]; }4void push(int x) {5if (size == data.length) data = Arrays.copyOf(data, data.length * 2); // resizable; a fixed stack rejects instead6data[size++] = x;7}8int pop() {9if (size == 0) throw new NoSuchElementException("stack underflow");10return data[--size];11}12int peek() {13if (size == 0) throw new NoSuchElementException("stack underflow");14return data[size - 1]; // look without removing15}16boolean isEmpty() { return size == 0; }17}
Live state
size
0
capacity
3
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | size=0 capacity=3 | An empty stack backed by an array of 3. The top is data[size - 1]; push and pop only touch the end, so both are O(1). |
| 2 | L6 | op=push(5) size=1 top=5 | data[size++] = 5: 5 is the new top. |
| 3 | L6 | op=push(8) size=2 top=8 | data[size++] = 8: 8 is the new top. |
| 4 | L6 | op=push(3) size=3 top=3 | data[size++] = 3: 3 is the new top. |
| 5 | L14 | op=peek() top=3 size=3 | peek() returns 3 without removing it: size stays 3. |
| 6 | L10 | op=pop() returned=3 size=2 | Return data[--size] = 3: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 7 | L10 | op=pop() returned=8 size=1 | Return data[--size] = 8: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 8 | L10 | op=pop() returned=5 size=0 | Return data[--size] = 5: the most recently pushed value leaves first (LIFO). The slot is simply ignored from now on. |
| 9 | L9 | op=pop() size=0 errors=1 | pop() on an empty stack: underflow. Java's ArrayDeque.pop throws NoSuchElementException; poll() would return null. |
| 10 | L6 | op=push(7) size=1 top=7 | data[size++] = 7: 7 is the new top. |
| 11 | L16 | size=1 errors=1 return=[7] | Final stack (bottom to top): [7]; 1 rejected operation. |
Linked list stack
A linked-list stack makes the top the head of a singly linked list: push inserts a new head, pop removes the head. Every operation is a true O(1) with no resizing, but each element costs a node object and the nodes are scattered in memory. It never needs to copy elements and never wastes spare capacity.
Use it when worst-case O(1) per operation matters more than speed and memory.
1class LinkedStack {2private static class Node { int val; Node next; Node(int v, Node n) { val = v; next = n; } }3private Node top;4void push(int x) { top = new Node(x, top); }5int pop() {6if (top == null) throw new NoSuchElementException("stack underflow");7int v = top.val; top = top.next; return v;8}9boolean isEmpty() { return top == null; }10}
ArrayDeque in Java
java.util.ArrayDeque is the standard stack in modern Java: a resizable circular array with O(1) amortised operations at both ends. Use it through the Deque interface: push, pop and peek work at the front (the top). It rejects null elements and is not thread-safe. It is usually faster than both Stack and LinkedList.
Declare the variable as Deque so the implementation can change.
1Deque<Integer> stack = new ArrayDeque<>();2stack.push(1); stack.push(2); stack.push(3);3int top = stack.peek(); // 34int popped = stack.pop(); // 35boolean empty = stack.isEmpty();
Why legacy Stack is usually avoided
java.util.Stack extends Vector, so it inherits dozens of unrelated list methods (such as add(index, e) and get(i)) that break the LIFO contract, and every method is synchronised, which costs time you rarely need. Its push adds at the end, so iteration order is bottom to top, the opposite of ArrayDeque. The JDK documentation itself recommends Deque implementations instead.
Use ArrayDeque unless you are maintaining old code.
1Stack<Integer> legacy = new Stack<>();2legacy.push(1); legacy.push(2);3System.out.println(legacy); // [1, 2]: bottom to top4legacy.add(0, 99); // allowed, and violates the idea of a stack5Deque<Integer> modern = new ArrayDeque<>();6modern.push(1); modern.push(2);7System.out.println(modern); // [2, 1]: top first
Practice ladder
Now try it yourself
Easy
- Implement push and pop for an array stack
- Which end of an ArrayDeque does push use?
Medium
- Implement a linked-list stack with size()
- List three problems with java.util.Stack
Advanced
- Benchmark Stack, ArrayDeque and LinkedList as stacks for 10⁷ push/pop pairs
Key takeaways
- Array stacks are compact; linked stacks never resize.
- ArrayDeque (through Deque) is the default stack in Java.
- Avoid java.util.Stack: legacy, synchronised and not LIFO-only.