What Is a Java Stack?

A Java stack is a collection that stores elements in LIFO (last in, first out) order: the element added most recently is the first one removed. The usual analogy is a stack of plates. You add a plate to the top, and when you need one, you take it from the top — never from the middle.

One clarification before going further. This article is about the stack as a data structure, implemented in Java by the java.util.Stack class. It is not about the JVM's stack memory, the region that holds method frames and local variables and is usually discussed alongside the heap. It is also not about "full-stack Java" development. Those are three unrelated meanings of the same word.

So what is a stack in Java exactly, how does it work, and is java.util.Stack actually the class you should be using?

What Is a Stack in Java?

A stack is an abstract data type built around three operations: push adds an element to the top, pop removes and returns the element at the top, and peek looks at the top element without removing it. Every insertion and every removal happens at the same end of the collection. There is no efficient way to reach an element in the middle, and that constraint is the point — it makes the structure's behavior predictable.

Stacks appear throughout software. Undo histories in editors are stacks: the last change you made is the first one reversed. Depth-first search and backtracking algorithms use a stack to remember where to return to. Expression parsers use stacks to match brackets and evaluate operators. The JVM itself uses a call stack to track method invocations, which is where the term "stack trace" comes from.

Java implements the data structure in the java.util.Stack class, which has been part of the standard library since JDK 1.0. Stack extends Vector, so it is backed by a resizable array and inherits Vector's synchronized access.

Stacks are often introduced alongside their mirror image, the Java queue, which stores elements in FIFO (first in, first out) order instead.

Java Stack Methods

java.util.Stack adds five methods to those it inherits from Vector:

MethodDescriptionComplexity
push(E item)Adds an item to the top of the stack and returns it.O(1) amortized
pop()Removes and returns the element at the top. Throws EmptyStackException if the stack is empty.O(1)
peek()Returns the element at the top without removing it. Throws EmptyStackException if the stack is empty.O(1)
empty()Returns true if the stack contains no elements.O(1)
search(Object o)Returns the 1-based position of an object measured from the top of the stack, or -1 if it is not present.O(n)

Two details catch people out. First, pop() and peek() throw EmptyStackException rather than returning null, so an empty stack must be checked before reading from it. Second, search() counts from the top down and starts at 1, not from the bottom up and not from 0 — the element you would get from peek() is at position 1.

Because Stack extends Vector, it also inherits the entire Vector API: get(int index), add(int index, E element), elementAt(int index), size(), contains(), and the rest.

How to Implement a Stack in Java

The direct approach uses the Stack class:

Stack<String> stack = new Stack<>();

stack.push("first");
stack.push("second");
stack.push("third");

String top = stack.peek();            // "third" — still on the stack
String last = stack.pop();            // "third" — removed
boolean isEmpty = stack.empty();      // false
int position = stack.search("first"); // 2

The modern approach uses ArrayDeque, which supports the same three operations through the Deque interface:

Deque<String> stack = new ArrayDeque<>();

stack.push("first");
stack.push("second");

String top = stack.peek();  // "second"
String last = stack.pop();  // "second"

The method names are identical. The behavior on an empty collection differs: ArrayDeque.pop() throws NoSuchElementException, and peek() returns null rather than throwing.

Java Stack vs Deque: Which Should You Use?

For new code, use a Java deque. Oracle's own documentation for java.util.Stack recommends Deque and its implementations in preference to the Stack class. There are three reasons.

Inheritance leaks the abstraction. Because Stack extends Vector, callers can bypass LIFO access entirely — stack.get(0) reads the bottom element, and stack.add(1, item) inserts into the middle. A type whose contract can be sidestepped by its own public API is a weak guarantee.

Iteration order is backwards. Iterating a Stack walks it bottom to top, which is the reverse of the order pop() returns elements. Iterating an ArrayDeque used as a stack walks from the head, matching pop order. The Stack behavior is a frequent source of quiet bugs.

Synchronization you probably don't want. Stack declares pop(), peek() and search() as synchronized, and most of the Vector methods underneath them are synchronized too, so single-threaded code pays for locking it never uses. That locking is also per-method, which means compound operations such as "peek, then pop if it matches" are still not atomic. You get the cost without the guarantee.

ArrayDeque has none of these problems and is generally faster.

Stack vs Queue

Stacks and queues both restrict where you can add and remove elements. They differ in which end each operation uses:

StackQueue
OrderLIFO (last in, first out)FIFO (first in, first out)
Insert atTopTail
Remove fromTopHead
Ends usedSame end for bothOpposite ends
Typical usesUndo history, backtracking, depth-first search, expression parsingTask queues, message buffering, breadth-first search

A Java deque is the general case of both: it permits insertion and removal at either end, so a single deque can serve as a stack, a queue, or both at once.

Java Stacks in Valkey and Redis

An in-memory Stack or ArrayDeque lives inside one JVM. It disappears when the process restarts, and it is invisible to every other instance of your application. That is fine for a parser or a local algorithm, and a problem as soon as the state needs to be shared — a collaborative undo history across a cluster, a retry stack that must survive a deployment, or backtracking state shared by several workers.

Valkey and Redis have no dedicated stack type, but they don't need one. A stack is modeled on a Redis list using two commands that operate on the same end:

  • LPUSH adds an element to the head of the list.
  • LPOP removes and returns the element at the head.

Pushing and popping at the same end produces LIFO order. Using opposite ends instead — LPUSH with RPOP — produces FIFO order and gives you a Redis queue. Both commands run in constant time and are atomic, so multiple clients can operate on the same stack without corrupting it.

Distributed Java Stacks with Redisson

Working with raw commands means giving up the Java collection interfaces you already know. Redisson, a Valkey and Redis Java client, closes that gap by implementing the standard interfaces on top of Valkey and Redis data structures.

Redisson does not ship a separate RStack type, and it doesn't need to: java.util.Deque already defines push(), pop() and peek() as stack operations, and RDeque implements java.util.Deque. An RDeque used through those three methods is a distributed stack.

RedissonClient redisson = Redisson.create();

RDeque<String> stack = redisson.getDeque("undoHistory");

stack.push("edit-1");
stack.push("edit-2");

String top = stack.peek();  // "edit-2" — still on the stack
String last = stack.pop();  // "edit-2" — removed

Any application instance connected to the same Valkey or Redis deployment sees the same stack. RDeque is thread-safe, and unlike java.util.Stack it is safe across processes as well as threads.

For workers that need to wait for elements rather than poll for them, RBlockingDeque adds blocking operations:

RBlockingDeque<String> tasks = redisson.getBlockingDeque("tasks");

tasks.push("task-1");

// blocks until an element is available; throws InterruptedException
String task = tasks.takeFirst();

Pair push() with takeFirst() to keep LIFO ordering — both act on the head of the deque. Pairing put() with takeFirst() would insert at the tail and give you a FIFO queue instead.

Redisson also provides asynchronous, Reactive and RxJava3 interfaces for these objects through RDequeAsync, RDequeReactive and RDequeRx, so a distributed stack fits into non-blocking applications without tying up threads.

To learn more about Redisson, RDeque, and the advanced features of Redisson PRO, visit the Redisson website today.

Similar Terms