☕ 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 foundArrayDeque 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());// false — 10 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 codeCommon 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 noneRelated Topics in Collections Framework
LinkedHashMap
LinkedHashMap extends HashMap and maintains a doubly-linked list running through all its entries, preserving the order in which entries were inserted. Iteration over a LinkedHashMap always returns entries in insertion order. Optionally, it can be constructed in access-order mode where entries are ordered by most-recently accessed, making it the perfect foundation for implementing a Least Recently Used (LRU) cache. LinkedHashMap has slightly higher memory overhead than HashMap due to the extra linked list pointers.
TreeMap
TreeMap is a Red-Black tree implementation of the NavigableMap interface. It stores key-value pairs in sorted order — either the natural ordering of keys (requiring them to implement Comparable) or a custom Comparator provided at construction. All basic operations (get, put, remove, containsKey) are O(log n). TreeMap provides rich navigation operations: finding the closest key, extracting sub-maps, and headMap/tailMap views.
Hashtable
Hashtable is a legacy hash table implementation that predates the Collections Framework. Like Vector for lists, Hashtable was Java's original hash map from Java 1.0. It is thread-safe because all its methods are synchronised, but it does not permit null keys or null values. Hashtable has been retrofitted to implement the Map interface but should never be used in new code — HashMap is the non-concurrent replacement and ConcurrentHashMap is the concurrent replacement.
ConcurrentHashMap
ConcurrentHashMap is a thread-safe, high-performance hash map optimised for concurrent access. Unlike Hashtable and synchronised HashMap (which use a single lock), ConcurrentHashMap uses a more sophisticated concurrency mechanism — in Java 8+, it uses CAS (compare-and-swap) operations and per-bucket synchronisation — allowing multiple threads to read and write to different parts of the map simultaneously without blocking each other. It does not permit null keys or null values.