Java Stack: Prefer ArrayDeque Over Stack
In Java, the clean default for stack-style code is Deque with ArrayDeque. It is faster to read, avoids the old synchronized Stack API, and works well for coding practice, parsers, monotonic stacks, and small in-memory workflows.
The Short Rule
| Situation | Use |
|---|---|
| Single-threaded stack or queue | ArrayDeque |
| Multi-threaded blocking handoff | LinkedBlockingDeque |
| Multi-threaded non-blocking deque | ConcurrentLinkedDeque |
| Legacy code only | Stack |
For most algorithm problems, use:
Deque<Integer> stack = new ArrayDeque<>();
Then use stack operations:
stack.push(10);
stack.push(20);
int top = stack.peek(); // 20
int value = stack.pop(); // 20
Why Not Stack
java.util.Stack is an older class built on top of Vector. Its methods are synchronized because it comes from an older collection design.
That synchronization is usually not useful in normal algorithm code:
- the stack is local to one method
- only one thread can access it
- the synchronization adds noise to the mental model
- the API carries legacy
Vectorbehavior
Use Stack only when maintaining old code that already depends on it.
Why ArrayDeque
ArrayDeque was added in Java 6 and is the normal replacement for stack and queue use cases when you do not need thread safety.
It gives you both ends of a deque:
Deque<Character> stack = new ArrayDeque<>();
stack.push('a'); // add to front
stack.push('b');
stack.peek(); // b
stack.pop(); // b
It also works as a queue:
Deque<String> queue = new ArrayDeque<>();
queue.offerLast("first");
queue.offerLast("second");
String next = queue.pollFirst(); // first
That makes ArrayDeque useful across many patterns:
- valid parentheses
- monotonic stack
- breadth-first search queue
- undo buffers
- parser state
- adjacent duplicate removal
Single-Threaded Code
If the structure is owned by one method, one request, or one worker, use ArrayDeque.
import java.util.ArrayDeque;
import java.util.Deque;
class Example {
boolean hasBalancedParentheses(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(') {
stack.push(c);
} else if (c == ')') {
if (stack.isEmpty()) {
return false;
}
stack.pop();
}
}
return stack.isEmpty();
}
}
This is the common shape for coding interviews and LeetCode-style problems.
Multi-Threaded Code
If multiple threads share the deque, choose the concurrency behavior explicitly.
Use LinkedBlockingDeque when producers and consumers should wait:
BlockingDeque<Event> queue = new LinkedBlockingDeque<>();
queue.putLast(event); // producer may block
Event next = queue.takeFirst(); // consumer waits if empty
Use ConcurrentLinkedDeque when operations should be non-blocking:
Deque<Event> deque = new ConcurrentLinkedDeque<>();
deque.offerLast(event);
Event next = deque.pollFirst();
The difference matters:
| Need | Better fit |
|---|---|
| Backpressure and waiting consumers | LinkedBlockingDeque |
| Lock-free concurrent add/remove | ConcurrentLinkedDeque |
| Local algorithm state | ArrayDeque |
Virtual Threads And Pinning
Project Loom changed how Java developers think about blocking. Virtual threads make blocking code cheaper, so code can often stay direct instead of becoming callback-heavy.
The old warning was: be careful with blocking inside synchronized code, because it could pin a virtual thread to its carrier thread. That specific synchronized pinning issue was addressed in Java 24 by JEP 491.
Java 26 adds another related improvement: virtual threads no longer stay pinned while waiting for another thread to finish class initialization.
So the practical rule is not "never synchronize because of Loom." The better rule is:
- use
ArrayDequefor local stack state because it is simple and unsynchronized - use concurrent collections when state is actually shared
- keep blocking coordination explicit
- test real virtual-thread workloads with current JDK behavior
ArrayDeque is still the best default stack choice, but the reason is mostly clarity and fit, not fear of synchronized in modern Java.
Quick Pattern
Use this import block for most stack problems:
import java.util.ArrayDeque;
import java.util.Deque;
Use this declaration:
Deque<Integer> stack = new ArrayDeque<>();
Use these operations:
| Action | Method |
|---|---|
| Push | stack.push(value) |
| Read top | stack.peek() |
| Pop | stack.pop() |
| Check empty | stack.isEmpty() |
Takeaways
- Prefer
Dequeover legacyStack. - Use
ArrayDequefor single-threaded stack and queue logic. - Use
LinkedBlockingDequewhen threads need blocking producer-consumer behavior. - Use
ConcurrentLinkedDequewhen shared access should be non-blocking. - For virtual threads, avoid outdated pinning advice and verify behavior against the JDK version you run.