Skip to content
Ameen Abozaid
PublishedBEG-040-L04

Stacks

  • Lesson 4 of 7
  • 9 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

A stack is the simplest useful data structure, and one of the most common. It is built on a rule: you may only add to the top and only remove from the top. The picture is a stack of plates in a canteen. You put a clean plate on top, and you take the top plate. You do not pull one from the middle.

The rule sounds like a limitation, but it is the point. Because every operation touches only the top, every operation is O(1), and the order of removal is always "newest first". Many problems have exactly that shape: undo, going back in a browser, and matching brackets.

By the end you will use a stack through Java's ArrayDeque, write a bracket checker, and know why the old Stack class is avoided.

Why this matters: the call stack that runs your Java methods is a stack, and so is the undo history of nearly every editor. Recognizing "newest first" in a problem tells you which structure to reach for.

This lesson builds on Lessons 1 to 3 (arrays and linked nodes can both implement a stack) and on the Queues and deques lesson of the Collections module (BEG-030).

Learning Objectives

By the end of this lesson you will be able to:

  • Define a stack and LIFO (last in, first out).
  • Use push, pop, and peek, and state that each is O(1).
  • Use ArrayDeque as a stack, and explain why Deque is preferred over Stack.
  • Predict what pop and peek do on an empty stack.
  • Implement a bracket-matching checker with a stack.
  • Recognize problems that need LIFO order.

Written Explanation

The three operations

OperationMeaningCost
push(x)Put x on topO(1)
pop()Remove and return the top itemO(1)
peek()Look at the top item without removing itO(1)

Worked trace:

text
push(1)        stack (top on the left): 1
push(2)        stack: 2 1
pop() -> 2     stack: 1
peek() -> 1    stack: 1   (unchanged)

The last item in (2) was the first out. That is LIFO: last in, first out.

Java: use ArrayDeque

The JDK documentation for the Deque interface says deques can be used as LIFO stacks, and that this interface should be used in preference to the legacy Stack class. It maps the stack operations to push, pop, and peek. ArrayDeque is the usual implementation, and its documentation says it is likely to be faster than Stack when used as a stack.

java
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
System.out.println(stack.pop());    // 2
System.out.println(stack.peek());   // 1

Declare the variable as Deque<Integer> (the interface) and create an ArrayDeque. This follows the habit from the Collections module: code against the interface, choose the implementation in one place.

Two behaviors on an empty stack, which you must know:

  • pop() throws NoSuchElementException.
  • peek() returns null (it does not throw).

Also, ArrayDeque refuses null items: push(null) throws NullPointerException. That is why peek() can use null to mean "empty" without ambiguity.

Why not java.util.Stack?

Stack is a legacy class from the earliest versions of Java. It is built on Vector, which was designed so that its methods are synchronized, and it exposes list operations such as get(i) that break the "only touch the top" idea. The Deque documentation steers you away from it. In new code, use ArrayDeque.

Implementing a stack yourself

An array stack is an array plus size where the top is data[size - 1]; push is a dynamic-array append (Lesson 2) and pop removes the last item, so both are O(1) (push is amortized). A linked stack is a linked list where push is addFirst and pop is removeFirst (Lesson 3), so both are O(1) worst case. The lab asks you to build one of them.

java
T pop() {
    if (size == 0) throw new NoSuchElementException("stack is empty");
    T top = (T) data[--size];
    data[size] = null;      // let the removed object be collected
    return top;
}

Problems that are secretly stacks

  • Undo: the last action is the first undone.
  • Back button: the last page visited is the first you return to.
  • The call stack: when a() calls b(), b must finish first. In BEG-038 you saw the stack of frames; StackOverflowError happens when it grows too deep.
  • Matching brackets and nested structures: the most recently opened bracket must be closed first.
  • Backtracking: try a path, and if it fails go back to the previous choice.

Bracket matching

Idea for "{[()]}": scan left to right.

  1. On an opener (, [, {: push it.
  2. On a closer: if the stack is empty, it is unbalanced; else pop and check that the popped opener matches this closer.
  3. At the end the stack must be empty; leftovers are unclosed openers.
java
static boolean balanced(String s) {
    Deque<Character> stack = new ArrayDeque<>();
    for (char c : s.toCharArray()) {
        if (c == '(' || c == '[' || c == '{') {
            stack.push(c);
        } else if (c == ')' || c == ']' || c == '}') {
            if (stack.isEmpty()) return false;
            char open = stack.pop();
            if ((c == ')' && open != '(') || (c == ']' && open != '[')
                    || (c == '}' && open != '{')) return false;
        }
    }
    return stack.isEmpty();
}

One pass, O(1) per character, so O(n) overall. Deque<Character> works because char is boxed to Character automatically.

Examples

Example 1: push, pop, peek, and the empty stack (simple)

java
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
System.out.println(stack.pop());
System.out.println(stack.peek());
stack.pop();
System.out.println(stack.peek());
stack.pop();    // on an empty stack

Real output (the last line is the exception, which the program caught and printed):

text
2
1
null
pop on empty: java.util.NoSuchElementException

Check the empty case with isEmpty() before pop, or accept the exception when empty means a bug.

Example 2: an undo history (practical)

java
Deque<String> history = new ArrayDeque<>();
history.push("typed A");
history.push("typed B");
history.push("deleted line");
System.out.println("undo: " + history.pop());
System.out.println("undo: " + history.pop());

Output:

text
undo: deleted line
undo: typed B

Each action is pushed as it happens. Undo pops the newest one. After two undos, only "typed A" remains. A real editor stores actions or snapshots rather than strings, but the shape is identical.

Example 3: validating bracket strings (realistic)

A configuration or expression checker calls balanced on each input. Running the method from above gave:

text
"{[()]}" -> true
"([)]" -> false
"((" -> false
"())" -> false
"" -> true

Look at why each result happens:

  • "([)]": the ) meets [ on top, a mismatch.
  • "((": the loop ends with two openers still on the stack.
  • "())": the second ) finds the stack empty.
  • "": nothing to match, so the stack is trivially empty: balanced.

This is the real task behind many parsers, from JSON and HTML to arithmetic.

Backend Insight

You will meet stacks in these places:

  • Stack traces. When an exception prints a trace, it is listing the call stack, from the most recent call down. Reading one is reading a stack.
  • Parsers and validators. Anything that reads nested text (JSON, XML, templates, SQL) tracks nesting, usually with an explicit stack or with recursion, which uses the call stack.
  • Undo and rollback. Features that let a user step backward often store operations in a stack.
  • Depth control. Recursion that never stops fills the call stack and ends in StackOverflowError. Converting a deep recursion into a loop with your own ArrayDeque is a standard fix, because your stack lives on the heap and can grow larger (up to available memory).

When you choose a stack, ask: "Do I always need the most recent item first?" If yes, it is a stack.

Common Mistakes

Mistake 1: Using java.util.Stack in new code

  • Incorrect: Stack<Integer> s = new Stack<>();
  • Correct: Deque<Integer> s = new ArrayDeque<>();

Mistake 2: Popping an empty stack

pop() on an empty ArrayDeque throws. Check isEmpty() first, as the bracket checker does before popping a closer.

Mistake 3: Forgetting the final emptiness check

If the loop finishes without a mismatch, "((" would look fine. The checker must return stack.isEmpty(), not true.

Mistake 4: Mixing up the iteration order

Printing an ArrayDeque used as a stack lists items from the top down: after push(1); push(2); push(3) it shows [3, 2, 1]. This is correct but surprising if you expect insertion order.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. After push(5), push(8), push(3), pop(), pop(), what does peek() return?

Show solution A1

A1. After the pushes the stack is 3, 8, 5 (top first). Two pops remove 3 and 8. peek() returns 5.

A2. What does pop() do on an empty ArrayDeque, and what does peek() do?

Show solution A2

A2. pop() throws NoSuchElementException; peek() returns null.

A3. Choose stack or not-a-stack: (a) the pages you can step back through, (b) the nearest unclosed { in code, (c) people paying at a till.

Show solution A3

A3. (a) stack, (b) stack, (c) not a stack: they are served in arrival order, which is a queue (next lesson).

Level B: Application

B1. Using the checker idea, say whether "[{}]", "{[}]", and "}{" are balanced, and why.

Show solution B1

B1. "[{}]": balanced; { pushed on top of [, } closes {, ] closes [, stack empty. "{[}]": not balanced; } meets [ on top. "}{": not balanced; the first } finds an empty stack.

B2. Write static String reverse(String s) using a Deque<Character>. What is its cost?

Show solution B2

B2.

java
static String reverse(String s) {
    Deque<Character> stack = new ArrayDeque<>();
    for (char c : s.toCharArray()) stack.push(c);
    StringBuilder sb = new StringBuilder();
    while (!stack.isEmpty()) sb.append(stack.pop());
    return sb.toString();
}

reverse("stack") returned kcats (real run). One push and one pop per character: O(n).

B3. Show the stack contents after each token while evaluating the postfix expression 3 4 + 2 * (numbers are pushed; an operator pops two numbers, applies itself, and pushes the result).

Show solution B3

B3. Tokens: 3 gives 3; 4 gives 4 3 (top first); + pops 4 and 3, pushes 7, giving 7; 2 gives 2 7; * pops 2 and 7, pushes 14, giving 14. The answer is 14.

Level C: Challenge

C1. Write static int evalPostfix(String expr) for expressions like "10 2 8 * + 3 -", with tokens separated by spaces and the operators +, -, *. Careful: for -, the second pop is the left operand.

Show solution C1

C1.

java
static int evalPostfix(String expr) {
    Deque<Integer> stack = new ArrayDeque<>();
    for (String token : expr.split(" ")) {
        if (token.equals("+") || token.equals("-") || token.equals("*")) {
            int b = stack.pop();    // right operand
            int a = stack.pop();    // left operand
            if (token.equals("+")) stack.push(a + b);
            else if (token.equals("-")) stack.push(a - b);
            else stack.push(a * b);
        } else {
            stack.push(Integer.parseInt(token));
        }
    }
    return stack.pop();
}

"3 4 + 2 *" gave 14 and "10 2 8 * + 3 -" gave 23 (real run). The second pop is the left operand because it was pushed earlier. The cost is O(n).

C2. Implement ArrayStack<T> with push, pop (throws NoSuchElementException if empty), peek (returns null if empty), and isEmpty, on a growing Object[]. Why set the freed slot to null on pop?

Show solution C2

C2.

java
static class ArrayStack<T> {
    private Object[] data = new Object[2];
    private int size;

    void push(T item) {
        if (size == data.length) data = Arrays.copyOf(data, size * 2);
        data[size++] = item;
    }

    @SuppressWarnings("unchecked")
    T pop() {
        if (size == 0) throw new NoSuchElementException("stack is empty");
        T top = (T) data[--size];
        data[size] = null;
        return top;
    }

    @SuppressWarnings("unchecked")
    T peek() { return size == 0 ? null : (T) data[size - 1]; }

    boolean isEmpty() { return size == 0; }
}

Pushing a, b, c then popping twice returned c, b, left a on top, and a pop on the empty stack threw "stack is empty" (real run). Setting the slot to null means the array no longer holds a reference to the removed object, so the garbage collector can free it. The cast is unchecked because Object[] cannot be generic; that is why the annotation is there.

Mini Challenge

Write a tiny text-editor undo model. Keep a StringBuilder text and a Deque<String> of earlier versions. type(String s) pushes the current text and then appends s; undo() pops and restores, doing nothing if the history is empty.

  1. Type "Hello", then " world", then "!".
  2. Print the text after each step.
  3. Undo twice and print again.

Check your work: you expect Hello, Hello world, Hello world!, and after two undos Hello. Note the cost of type in memory: copying whole strings is simple but not how a real editor does it.

Summary & Next Lesson

Key takeaways

  • A stack adds and removes at the top only: LIFO; push, pop, peek are O(1).
  • In Java use Deque<T> stack = new ArrayDeque<>(), not the legacy Stack class.
  • pop() on empty throws NoSuchElementException; peek() on empty returns null.
  • Bracket matching: push openers, pop on closers and compare, and require an empty stack at the end.
  • Undo, back, the call stack, and nested parsing are all stack problems.

Next lesson

Next is "Queues". A queue flips the rule: items leave from the front in arrival order, like a line at a shop.

Self-check

  1. What does LIFO mean?
  2. Which three operations does a stack offer and what do they cost?
  3. Why is ArrayDeque preferred over Stack?
  4. What does peek() return on an empty ArrayDeque?
  5. Why does a bracket checker return stack.isEmpty() at the end?
Show answers
  1. Last in, first out: the newest item comes out first.
  2. push, pop, peek; each is O(1).
  3. The JDK documentation recommends Deque over the legacy Stack class, and ArrayDeque is likely to be faster.
  4. null.
  5. Leftover openers mean the string is unbalanced, as in "((".

References

  • Oracle, Java SE 21 API, java.util.Deque. Supports: a deque can be used as a LIFO stack, it should be used in preference to the legacy Stack class, and push, pop, and peek act on the beginning of the deque.
  • Oracle, Java SE 21 API, java.util.ArrayDeque. Supports: null elements are prohibited, and it is likely to be faster than Stack when used as a stack.
  • Outputs were produced by running the code on JDK 21. The statement that Stack is built on Vector with synchronized methods is from general Java knowledge and was not re-checked on a fetched page for this lesson.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.