Java Stack push, pop, and peek: Core LIFO Operations
java stack push pop peek: Learn how push, pop, and peek work in Java's Stack class, including empty-stack behavior, thread-safety tradeoffs, and when ArrayDeque is a b...
java stack push pop peek requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When working with a Java stack, the three operations you will use most are push, pop, and peek. java.util.Stack implements these as instance methods that maintain a last-in-first-out (LIFO) ordering: push adds an element to the top, pop removes and returns the top element, and peek returns the top element without removing it. Their behavior looks simple, but the empty-stack handling, synchronization overhead, and the availability of ArrayDeque as a modern alternative make the choice more interesting than it first appears.
The Stack Class and Its LIFO Contract
java.util.Stack extends java.util.Vector, which means it inherits all of Vector's methods and its internal array-based storage. The class adds five methods: push, pop, peek, empty, and search. For most developers, the first three are the ones that matter daily.
The LIFO contract is simple: the most recently pushed element is the first one returned by pop or peek. This ordering is guaranteed by the implementation because elements are always added to and removed from the same end of the underlying array.
Stack<String> stack = new Stack<>(); stack.push("first"); stack.push("second"); stack.push("third"); System.out.println(stack.peek()); // third System.out.println(stack.pop()); // third System.out.println(stack.pop()); // second System.out.println(stack.pop()); // first
After this sequence, the stack is empty. The order of removal is exactly the reverse of the order of insertion.
push(): Adding an Element to the Top
The push method adds an element to the top of the stack and returns the element that was added:
String result = stack.push("item"); System.out.println(result); // "item"
The return value is often ignored, but it is useful when you want to chain operations or when you need the pushed value in an expression. The method accepts null as a value, because Vector allows null elements. Whether that is desirable depends on your application; if your stack represents a sequence of required values, a null check before pushing is usually clearer than relying on peek to detect missing data.
Pushing onto a Stack never throws an exception under normal conditions. The underlying Vector grows its capacity automatically when the internal array is full, so there is no fixed-size limit that the caller must manage.
pop(): Removing the Top Element
pop removes the element at the top of the stack and returns it. This is the only stack-specific way to remove an element; the inherited Vector methods such as remove(int) and remove(Object) also work, but they bypass the LIFO discipline and should generally be avoided when you intend to use the object as a stack.
Stack<Integer> stack = new Stack<>(); stack.push(10); stack.push(20); int value = stack.pop(); System.out.println(value); // 20 System.out.println(stack.size()); // 1
If the stack is empty when pop is called, it throws EmptyStackException. This is a RuntimeException, so the compiler does not force you to catch it. In production code, you should either check empty() before calling pop or catch the exception where the empty state is a realistic condition.
peek(): Inspecting the Top Element Without Removing It
peek returns the element at the top of the stack but leaves the stack unchanged. This is the operation to use when you need to look at the most recent element without consuming it, such as when implementing an undo history where the current state must remain available for a later pop.
Stack<String> history = new Stack<>(); history.push("state-A"); history.push("state-B"); String current = history.peek(); System.out.println(current); // "state-B" System.out.println(history.size()); // 2
Like pop, peek throws EmptyStackException when the stack is empty. The difference is that peek has no side effect on the stack contents, so a failed peek leaves the stack in the same state it was in before the call.
What Happens When the Stack Is Empty
Both pop and peek throw EmptyStackException when called on an empty stack. empty() returns a boolean that tells you whether the stack has no elements, and size() tells you the exact count. The empty method is defined on Stack itself, while size comes from Vector.
Stack<String> stack = new Stack<>(); if (!stack.empty()) { String value = stack.pop(); } else { System.out.println("Stack is empty"); }
The empty() check and the pop() call are not atomic. In a single-threaded program this is fine, but if multiple threads share the same stack, the check-then-act sequence can race. That leads to the thread-safety question in the next section.
Thread Safety and Synchronization Overhead
Because Stack extends Vector, every method is synchronized on the stack instance. This makes individual push, pop, and peek calls safe from concurrent modification, but it does not make compound operations safe. A sequence like if (!stack.empty()) stack.pop(); is not atomic; another thread can remove the last element between the check and the pop, causing EmptyStackException anyway.
The synchronization also carries a cost. Every call acquires a monitor lock, even when the stack is used by only one thread. In single-threaded code, this is unnecessary overhead. For concurrent use, the per-method locking is rarely sufficient, and you would typically need external synchronization or a different structure such as ConcurrentLinkedDeque or a BlockingDeque implementation.
For a single-threaded LIFO structure, ArrayDeque is the commonly recommended replacement. It has no synchronization overhead and provides addFirst, removeFirst, and peekFirst methods that mirror the stack behavior.
Stack vs ArrayDeque: Choosing the Right LIFO Implementation
java.util.Stack is a legacy class. The Java documentation for ArrayDeque explicitly notes that it is preferable to Stack when a LIFO stack is needed. The table below summarizes the practical differences.
| Criterion | Stack | ArrayDeque |
|---|---|---|
| Inheritance | Extends Vector | Implements Deque |
| Synchronization | Synchronized per method | Not synchronized |
| Null elements | Allowed | Not allowed |
| LIFO methods | push, pop, peek | addFirst, removeFirst, peekFirst |
| Empty behavior | Throws EmptyStackException | removeFirst throws NoSuchElementException |
The ArrayDeque methods are not named push and pop, but the behavior is the same: addFirst adds to the head, removeFirst removes from the head, and peekFirst inspects the head. One behavioral difference worth noting is that peekFirst returns null on an empty deque, whereas peek on a Stack throws an exception.
ArrayDeque<String> stack = new ArrayDeque<>(); stack.addFirst("first"); stack.addFirst("second"); String top = stack.peekFirst(); // "second" String removed = stack.removeFirst(); // "second"
The choice depends on your constraints. If you need null elements, Stack supports them and ArrayDeque does not. If you need thread-safe individual operations without external locking, Stack gives you that through its inherited synchronization, though at a performance cost. If you are writing single-threaded code and want the modern API, ArrayDeque is the cleaner choice.
Common Pitfalls When Using push, pop, and peek
One recurring mistake is using the inherited Vector methods on a Stack instance. Methods like add(int, E), remove(int), and get(int) let you access elements in the middle of the stack, which breaks the LIFO invariant. If you need indexed access, a Stack is the wrong structure; use a List implementation instead.
Another pitfall is confusing peek with pop in a loop. A loop that calls peek without pop will never terminate and will keep reading the same top element. The loop condition should be based on empty() or size(), and the loop body should call pop when it intends to consume the element.
// Correct: consumes each element while (!stack.empty()) { process(stack.pop()); } // Incorrect: reads the same element forever while (!stack.empty()) { process(stack.peek()); }
The second loop compiles and runs, but it never removes anything, so it processes the top element repeatedly until the program is interrupted. This is the kind of bug that is easy to miss in code review because the logic looks plausible.
Using search() to Locate an Element
The Stack class also provides search(Object o), which returns the 1-based position of the element from the top of the stack, or -1 if the element is not present. The top element is at position 1, the next one down is at position 2, and so on.
Stack<String> stack = new Stack<>(); stack.push("a"); stack.push("b"); stack.push("c"); System.out.println(stack.search("c")); // 1 System.out.println(stack.search("a")); // 3 System.out.println(stack.search("z")); // -1
This method is rarely necessary, but it is useful when you need to know how far an element is from the top, such as when implementing a bounded history where you want to discard elements beyond a certain depth. Note that search uses equals for comparison, so it works with custom objects as long as their equals method is implemented correctly.