☕ Java

Stack

Stack is a legacy class that extends Vector and represents a last-in, first-out (LIFO) stack of objects. It adds five stack-specific methods on top of Vector: push(), pop(), peek(), empty(), and search(). Because Stack extends Vector, it inherits all of Vector's methods including all List operations and random access — which is semantically wrong for a stack data structure. For new code, the Deque interface (with ArrayDeque as implementation) is the recommended way to implement a stack in Java.

The Stack Class — Design and Limitations

The Stack class was introduced in Java 1.0 as a simple LIFO stack implementation. Its design choice — extending Vector — was a mistake that violates the Liskov Substitution Principle and the principle of encapsulation. A stack should expose only push, pop, peek, and isEmpty operations. By extending Vector, Stack exposes every List method: get(index), add(index, element), remove(index), and all other positional operations that fundamentally violate the stack's LIFO contract. Callers can access any element by index, insert at arbitrary positions, and remove from the middle — operations that make no sense for a stack. This is the classic composition-versus-inheritance design error: Stack should have contained a Vector (or array) and exposed only stack operations, not inherited from Vector and exposed all its operations. The Java designers acknowledged this mistake — the Javadoc for Stack 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." Stack inherits Vector's synchronisation, making every push(), pop(), and peek() a synchronised method call. Like Vector, this synchronisation is excessive for single-threaded use and insufficient for compound operations in multi-threaded use. The search(Object o) method returns the 1-based position of an element from the top of the stack — 1 means top of stack, 2 means one below the top, and so on. It returns -1 if not found. This method has an unusual 1-based convention different from all other Java indexing. Because of these design problems, Stack should never be used in new code. The correct replacement is Deque, specifically ArrayDeque for single-threaded use.
Java
// ── Stack basic usage: ───────────────────────────────────────────────
Stack<Integer> stack = new Stack<>();

// Push elements:
stack.push(10);
stack.push(20);
stack.push(30);
System.out.println(stack);     // [10, 20, 30] — bottom to top

// Peek — view top without removing:
System.out.println(stack.peek());    // 30 — top of stack
System.out.println(stack.size());    // 3 — unchanged

// Pop — remove and return top:
System.out.println(stack.pop());     // 30
System.out.println(stack.pop());     // 20
System.out.println(stack.pop());     // 10

// empty():
System.out.println(stack.empty());   // true

// pop() on empty — throws EmptyStackException:
try {
    stack.pop();
} catch (EmptyStackException e) {
    System.out.println("Stack is empty");
}

// ── The design problem — inherited List operations break abstraction: ──
Stack<String> s = new Stack<>();
s.push("bottom");
s.push("middle");
s.push("top");

// These make no sense for a stack:
s.add(1, "inserted-in-middle");  // insert at index 1 — violates LIFO!
s.remove(0);                      // remove bottom element — violates LIFO!
String arbitrary = s.get(0);      // access by index — violates LIFO!

// search() — 1-based from top:
s = new Stack<>();
s.push("a"); s.push("b"); s.push("c");
System.out.println(s.search("c"));  // 1 — top
System.out.println(s.search("b"));  // 2 — one below top
System.out.println(s.search("a"));  // 3 — bottom
System.out.println(s.search("z"));  // -1 — not found

ArrayDeque as a Modern Stack

The Javadoc for both Stack and Deque explicitly recommend using Deque for stack operations in new code. ArrayDeque is the recommended concrete implementation — it is backed by a resizable array, is not synchronised, does not expose non-stack operations when used through the Deque interface, and is faster than Stack for all operations. When using a Deque as a stack, use push() and pop() methods which operate on the front (head) of the deque. push(element) is equivalent to addFirst(element), and pop() is equivalent to removeFirst(). peek() views the first element without removing it. This is semantically identical to a LIFO stack. The key discipline is to declare the variable as Deque<E> rather than ArrayDeque<E>. This restricts callers to only the Deque interface operations and prevents them from calling the List-like operations that ArrayDeque might offer if accessed through a more specific type. Deque provides push(), pop(), peek(), isEmpty(), and size() — everything a stack should expose and nothing more. The absence of synchronisation in ArrayDeque is a feature — it is faster in single-threaded code, which is where the vast majority of stack usage occurs (expression evaluation, recursive algorithm simulation, backtracking, undo functionality). For a thread-safe stack, use a Deque wrapped in Collections.synchronizedDeque (if available) or use the concurrent deque implementations from java.util.concurrent.
Java
// ── ArrayDeque as a stack — the modern replacement for Stack: ────────
Deque<Integer> stack = new ArrayDeque<>();  // declared as Deque

stack.push(10);    // addFirst — O(1)
stack.push(20);
stack.push(30);

System.out.println(stack.peek());   // 30 — view top, no removal
System.out.println(stack.pop());    // 30 — remove top
System.out.println(stack.pop());    // 20
System.out.println(stack.isEmpty());// false10 remains

// pop() on empty — throws NoSuchElementException (not EmptyStackException):
stack.pop(); // removes 10
try {
    stack.pop();
} catch (NoSuchElementException e) {
    System.out.println("Stack is empty");
}

// Null-safe alternative — returns null on empty:
Integer top = stack.pollFirst();  // null if empty — no exception

// ── Practical: balanced parentheses checker: ──────────────────────────
public static boolean isBalanced(String expression) {
    Deque<Character> stack = new ArrayDeque<>();

    for (char c : expression.toCharArray()) {
        if (c == '(' || c == '[' || c == '{') {
            stack.push(c);
        } else if (c == ')' || c == ']' || c == '}') {
            if (stack.isEmpty()) return false;
            char top = stack.pop();
            if ((c == ')' && top != '(') ||
                (c == ']' && top != '[') ||
                (c == '}' && top != '{')) {
                return false;
            }
        }
    }
    return stack.isEmpty();
}

System.out.println(isBalanced("({[]})"));  // true
System.out.println(isBalanced("({[})"));   // false

// ── Practical: reverse a string using a stack: ───────────────────────
public 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();
}

System.out.println(reverse("Hello"));  // olleH

// ── Stack vs Deque — why Deque is preferred: ─────────────────────────
//
//                Stack (legacy)    Deque/ArrayDeque (modern)
// ────────────── ───────────────   ─────────────────────────
// Extends         Vector            (uses composition)
// Thread safe     Yes (slow)        No (add sync if needed)
// Exposes List?   Yes (bad design)  No (clean interface)
// Performance     Slow              Fast
// Recommended?    Legacy only       Always for new code

Common Stack Algorithms

Despite the recommendation to use ArrayDeque, stack algorithms remain one of the most important categories of programming problems. Stacks are the natural data structure for problems involving nested structures, matching delimiters, expression evaluation, undo/redo systems, DFS traversal, and conversion between recursion and iteration. Knowing these patterns enables you to solve a wide class of problems efficiently. Expression evaluation uses two stacks: one for operators and one for operands. When the input is fully parsed, remaining operators are applied to operands in precedence order. This is the Shunting-Yard algorithm. Browser history (back button) is a stack of visited URLs. The call stack of a running program is literally a stack — each method invocation pushes a frame; each return pops one. Simulating recursion iteratively requires explicitly managing a stack that mirrors what the call stack would do. Monotonic stacks are a powerful advanced pattern. A monotonic stack maintains elements in a strictly increasing or decreasing order. When a new element violates the order, elements are popped until the order is restored or the stack is empty. This pattern efficiently solves "next greater element," "previous smaller element," and range query problems in O(n) time that would otherwise require O(n²) nested loops.
Java
// ── Simulate recursion with explicit stack — DFS tree traversal: ──────
public static void dfsIterative(TreeNode root) {
    if (root == null) return;

    Deque<TreeNode> stack = new ArrayDeque<>();
    stack.push(root);

    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        System.out.println(node.value);

        // Push right first so left is processed first (LIFO):
        if (node.right != null) stack.push(node.right);
        if (node.left  != null) stack.push(node.left);
    }
}

// ── Undo/Redo system: ─────────────────────────────────────────────────
public class TextEditor {
    private final StringBuilder text   = new StringBuilder();
    private final Deque<String>  undo  = new ArrayDeque<>();
    private final Deque<String>  redo  = new ArrayDeque<>();

    public void type(String chars) {
        undo.push(text.toString());   // save current state
        redo.clear();                  // typing clears redo history
        text.append(chars);
    }

    public void undo() {
        if (undo.isEmpty()) return;
        redo.push(text.toString());    // save for redo
        text.setLength(0);
        text.append(undo.pop());       // restore previous state
    }

    public void redo() {
        if (redo.isEmpty()) return;
        undo.push(text.toString());
        text.setLength(0);
        text.append(redo.pop());
    }

    public String getText() { return text.toString(); }
}

TextEditor ed = new TextEditor();
ed.type("Hello");
ed.type(" World");
System.out.println(ed.getText());  // Hello World
ed.undo();
System.out.println(ed.getText());  // Hello
ed.undo();
System.out.println(ed.getText());  // (empty)
ed.redo();
System.out.println(ed.getText());  // Hello

// ── Monotonic stack — next greater element: ──────────────────────────
public static int[] nextGreaterElement(int[] arr) {
    int n = arr.length;
    int[] result = new int[n];
    Arrays.fill(result, -1);               // default: -1 (no greater element)
    Deque<Integer> stack = new ArrayDeque<>();  // stack of indices

    for (int i = 0; i < n; i++) {
        // Pop indices whose elements are smaller than arr[i]:
        while (!stack.isEmpty() && arr[stack.peek()] < arr[i]) {
            result[stack.pop()] = arr[i];
        }
        stack.push(i);
    }
    return result;
}

int[] arr = {4, 5, 2, 10, 8};
System.out.println(Arrays.toString(nextGreaterElement(arr)));
// [5, 10, 10, -1, -1]
// 4's next greater is 5, 5's is 10, 2's is 10, 10 and 8 have none

Related Topics in Collections Framework

Collections Overview
The Java Collections Framework (JCF) is a unified architecture for representing and manipulating groups of objects. It provides a set of interfaces that define the operations a collection must support, a set of abstract classes that provide partial implementations, and a set of concrete implementations optimised for different use cases. Every Java developer uses collections daily — lists for sequences, sets for uniqueness, maps for key-value pairs, and queues for ordering — and choosing the right implementation for the right use case is one of the most fundamental practical skills in Java.
Iterable
Iterable<E> is the root interface of the Java Collections hierarchy. Any class that implements Iterable can be used in a for-each loop. It declares a single abstract method: iterator(), which returns an Iterator<E> that the for-each loop uses to traverse the elements. Implementing Iterable is all that is required to make a custom data structure work with Java's enhanced for loop, the Stream API, and any method that accepts an Iterable.
Collection Interface
Collection<E> is the root interface of the main collection hierarchy, extending Iterable<E>. It defines the common operations that all collection types must support: adding elements, removing elements, checking containment, querying size, clearing, converting to an array, and bulk operations. List, Set, and Queue all extend Collection. Map does not extend Collection because a map operates on key-value pairs rather than individual elements.
ArrayList
ArrayList is a resizable-array implementation of the List interface. It is the most commonly used collection in Java, providing dynamic sizing on top of a standard array. Elements are stored in contiguous memory, enabling O(1) random access by index. When the internal array is full, ArrayList automatically allocates a larger array and copies all elements. ArrayList is not thread-safe and preserves insertion order, allowing duplicates.