Java Stack and Queue Explained — How, When, and Why to Use Each
Every non-trivial program needs to manage sequences of work — browser history, print jobs, undo operations, background task queues. The instant you need 'process this in a specific order', you're in Stack or Queue territory. Choosing the wrong one is the kind of subtle bug that doesn't crash your app — it just makes it behave weirdly in ways that are painful to diagnose six months later.
Both structures control HOW you access items in a collection. An ordinary List lets you grab any element by index, which is powerful but dangerous — nothing stops you from reaching into the middle and breaking an ordering contract. Stack and Queue enforce discipline: Stack guarantees LIFO (Last In, First Out) order, Queue guarantees FIFO (First In, First Out). That constraint is not a limitation; it's the whole point. It makes your intent explicit and your code self-documenting.
By the end of this article you'll know the difference between Java's legacy Stack class and the modern Deque-based approach, how to implement a browser back-button using a Stack, how to build a fair task scheduler with a Queue, which concrete classes to actually use in production, and three traps that catch even experienced devs off guard.
Stack in Java — LIFO Order and the Right Way to Use It
A Stack is a collection where the last element you push in is the first element you pop out — LIFO. Think of the call stack your JVM maintains right now: when method A calls method B, B goes on top. When B finishes, it's popped off and control returns to A. You can't return to A before B finishes. That's the contract.
Java ships with a class literally named Stack in java.util. The problem? It extends Vector, which is a synchronized legacy collection from the Java 1.0 era. Synchronization adds overhead even when you're working on a single thread, and the inheritance from Vector leaks methods like get(index) that completely bypass stack discipline. You could insert elements at position 0 — breaking LIFO silently.
The modern Java answer is ArrayDeque used as a stack. ArrayDeque is unsynchronized (faster), resizable, and exposes push/pop/peek semantics cleanly. Unless you're maintaining legacy code, reach for ArrayDeque every time. The only scenario where the old Stack class makes sense is when you explicitly need thread safety — and even then, prefer a concurrent data structure like ConcurrentLinkedDeque.
The three operations you care about are push (add to top), pop (remove from top, throws EmptyStackException if empty), and peek (view the top without removing). Always check isEmpty() before pop or peek unless you're happy catching exceptions as control flow — which you shouldn't be.
import java.util.ArrayDeque; import java.util.Deque; /** * Simulates a browser's Back button using a Stack. * Each time you visit a page, push its URL onto the history stack. * Hitting Back pops the current page and returns you to the previous one. */ public class BrowserHistory { public static void main(String[] args) { // ArrayDeque used as a Stack — modern, fast, no legacy baggage Deque<String> historyStack = new ArrayDeque<>(); // User navigates to three pages historyStack.push("https://google.com"); historyStack.push("https://thecodeforge.io"); historyStack.push("https://thecodeforge.io/java-collections"); System.out.println("Current page: " + historyStack.peek()); // view top without removing // User clicks Back — removes the current page from the top String leavingPage = historyStack.pop(); System.out.println("Left: " + leavingPage); System.out.println("Now on: " + historyStack.peek()); // User clicks Back again historyStack.pop(); System.out.println("Now on: " + historyStack.peek()); // Safe empty-check before popping — never pop blindly if (!historyStack.isEmpty()) { historyStack.pop(); } // Stack is now empty — peek would return null on ArrayDeque (not throw) System.out.println("History empty: " + historyStack.isEmpty()); System.out.println("Peek on empty ArrayDeque: " + historyStack.peek()); // returns null } }
Left: https://thecodeforge.io/java-collections
Now on: https://thecodeforge.io
Now on: https://google.com
History empty: true
Peek on empty ArrayDeque: null
Queue in Java — FIFO Order and Picking the Right Implementation
A Queue is a collection where the first element added is the first element removed — FIFO. Think of a print spooler: you sent your document first, so it prints first. No cutting in line.
Java models this with the Queue interface (in java.util), and it has several implementations. The two you'll use most are LinkedList and ArrayDeque. Both implement Queue. LinkedList is the traditional choice and works fine, but ArrayDeque is generally faster because it's backed by a resizable array with no node object overhead per element. Use ArrayDeque unless you specifically need null elements in your queue — ArrayDeque rejects nulls, LinkedList doesn't.
The Queue interface gives you two styles of every operation, and this distinction matters: - offer(e) adds an element, returns false if the queue is capacity-constrained and full. add(e) throws an exception instead. - poll() removes and returns the head, returns null if empty. remove() throws NoSuchElementException instead. - peek() returns the head without removing, returns null if empty. element() throws instead.
In production code, prefer offer/poll/peek — returning null is far less disruptive than throwing exceptions in hot paths. Use add/remove/element only when an empty or full queue is genuinely an unexpected exceptional case.
For multi-threaded scenarios, swap in a BlockingQueue implementation like LinkedBlockingQueue. It makes consumer threads wait automatically when the queue is empty — the foundation of every thread pool ever written.
import java.util.ArrayDeque; import java.util.Queue; /** * A simple task scheduler that processes support tickets in the order they arrive. * First ticket submitted = first ticket resolved. Classic FIFO. */ public class TaskScheduler { // Represents a single unit of work record SupportTicket(int id, String description) { @Override public String toString() { return "Ticket #" + id + " [" + description + "]"; } } public static void main(String[] args) { // ArrayDeque as a Queue — faster than LinkedList for most use cases Queue<SupportTicket> ticketQueue = new ArrayDeque<>(); // Customers submit support tickets at different times ticketQueue.offer(new SupportTicket(1001, "Login page not loading")); ticketQueue.offer(new SupportTicket(1002, "Payment fails at checkout")); ticketQueue.offer(new SupportTicket(1003, "Profile photo won't upload")); System.out.println("Tickets waiting: " + ticketQueue.size()); System.out.println("Next to process: " + ticketQueue.peek()); // look without removing System.out.println("\n--- Processing tickets in order ---"); // Process every ticket in FIFO order — first submitted, first resolved while (!ticketQueue.isEmpty()) { SupportTicket current = ticketQueue.poll(); // removes head, returns null if empty (safe) System.out.println("Resolving: " + current); } // poll() on an empty queue returns null — no exception SupportTicket ghost = ticketQueue.poll(); System.out.println("\nPolled empty queue: " + ghost); // null, not an exception } }
Next to process: Ticket #1001 [Login page not loading]
--- Processing tickets in order ---
Resolving: Ticket #1001 [Login page not loading]
Resolving: Ticket #1002 [Payment fails at checkout]
Resolving: Ticket #1003 [Profile photo won't upload]
Polled empty queue: null
Deque — When You Need a Stack and Queue in One
ArrayDeque implements Deque, which stands for Double-Ended Queue. It means you can add or remove from either end, making it simultaneously the best Stack and the best Queue in Java's standard library. This is worth understanding because it unifies the two data structures under one class.
When you use push/pop/peek on a Deque, you're treating it as a Stack (LIFO, operating on the head). When you use offer/poll/peek, you're treating it as a Queue (FIFO, adding to tail, removing from head). The same object, two personalities — which one it behaves as depends entirely on which methods you call.
This dual nature is genuinely useful. Imagine an undo/redo system: undo uses a stack (most recent action first). Redo is also a stack. But if you wanted to replay all actions in the order they happened — say, for an audit log — you'd iterate from the tail. A Deque handles all of these without switching data structures.
PriorityQueue is a separate beast worth mentioning here: it's a Queue that ignores insertion order and instead serves elements by priority (natural ordering or a custom Comparator). Use it for scheduling CPU tasks, Dijkstra's shortest path, or any problem where 'most important first' beats 'earliest first'.
import java.util.ArrayDeque; import java.util.Deque; /** * A text editor with Undo and Redo — both implemented with ArrayDeque. * Undo stack: tracks actions in reverse (most recent on top). * Redo stack: stores undone actions so they can be replayed. */ public class UndoRedoEditor { public static void main(String[] args) { // undoStack holds every action the user has performed, most recent on top Deque<String> undoStack = new ArrayDeque<>(); // redoStack holds actions the user has undone, so they can be redone Deque<String> redoStack = new ArrayDeque<>(); // User types three things performAction(undoStack, redoStack, "Typed: Hello"); performAction(undoStack, redoStack, "Typed: World"); performAction(undoStack, redoStack, "Bold: World"); System.out.println("=== Current undo history (top = most recent) ==="); System.out.println(undoStack); // User hits Ctrl+Z twice System.out.println("\n--- Undo ---"); undo(undoStack, redoStack); undo(undoStack, redoStack); // User hits Ctrl+Y once System.out.println("\n--- Redo ---"); redo(undoStack, redoStack); System.out.println("\n=== Final undo stack ==="); System.out.println(undoStack); System.out.println("=== Final redo stack ==="); System.out.println(redoStack); } static void performAction(Deque<String> undoStack, Deque<String> redoStack, String action) { undoStack.push(action); // push to top of undo stack redoStack.clear(); // any new action wipes the redo history (just like real editors) System.out.println("Did: " + action); } static void undo(Deque<String> undoStack, Deque<String> redoStack) { if (undoStack.isEmpty()) { System.out.println("Nothing to undo."); return; } String lastAction = undoStack.pop(); // remove from top of undo stack redoStack.push(lastAction); // move it to redo stack System.out.println("Undid: " + lastAction); } static void redo(Deque<String> undoStack, Deque<String> redoStack) { if (redoStack.isEmpty()) { System.out.println("Nothing to redo."); return; } String redonAction = redoStack.pop(); // take from top of redo stack undoStack.push(redonAction); // put back on undo stack System.out.println("Redid: " + redonAction); } }
Did: Typed: World
Did: Bold: World
=== Current undo history (top = most recent) ===
[Bold: World, Typed: World, Typed: Hello]
--- Undo ---
Undid: Bold: World
Undid: Typed: World
--- Redo ---
Redid: Typed: World
=== Final undo stack ===
[Typed: World, Typed: Hello]
=== Final redo stack ===
[Bold: World]
| Feature / Aspect | Stack (via ArrayDeque) | Queue (via ArrayDeque) |
|---|---|---|
| Order | LIFO — Last In, First Out | FIFO — First In, First Out |
| Add element | push(e) — adds to head/top | offer(e) — adds to tail |
| Remove element | pop() — removes from head/top | poll() — removes from head |
| Inspect without removing | peek() — views head/top | peek() — views head |
| Empty collection behaviour | peek() returns null, pop() throws | peek() returns null, poll() returns null |
| Backing class (recommended) | ArrayDeque | ArrayDeque or LinkedBlockingQueue (threaded) |
| Legacy alternative (avoid) | java.util.Stack (extends Vector) | LinkedList (slower, allows nulls) |
| Thread-safe variant | ConcurrentLinkedDeque | LinkedBlockingQueue / ArrayBlockingQueue |
| Real-world use case | Undo/redo, expression parsing, DFS | Task scheduling, print spoolers, BFS |
🎯 Key Takeaways
- Never use java.util.Stack in new Java code — it extends the outdated Vector class and exposes index-based methods that silently break LIFO order. Use Deque
backed by ArrayDeque instead. - ArrayDeque is both the best Stack and the best Queue in the standard library — push/pop makes it a stack, offer/poll makes it a queue. One class, two personas, zero overhead.
- Always prefer offer/poll/peek over add/remove/element when working with Queue — returning null on an empty collection is far safer than throwing NoSuchElementException in a production hot path.
- When threads are involved, forget ArrayDeque and reach for BlockingQueue implementations (LinkedBlockingQueue, ArrayBlockingQueue) — they handle inter-thread signalling automatically and eliminate the need for manual wait/notify loops.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Using java.util.Stack in new code — because Stack extends Vector, calling stack.get(0) or stack.remove(0) on it compiles and runs without error, silently destroying LIFO order. Fix: replace Stack
with Deque and instantiate it as new ArrayDeque<>(). The compiler will now refuse index-based access entirely. - ✕Mistake 2: Calling remove() or element() instead of poll() or peek() on an empty Queue — these throw NoSuchElementException at runtime, often surfacing as an uncaught exception in production. Fix: use poll() and peek() for null-safe access, then add an explicit null check after the call. Reserve the throwing variants for code where an empty queue is genuinely impossible.
- ✕Mistake 3: Storing null in an ArrayDeque — ArrayDeque.offer(null) throws NullPointerException immediately. This bites developers who migrate from LinkedList (which accepts nulls) to ArrayDeque. Fix: never use null as a sentinel value in a queue. Model 'no value' with Optional
or a dedicated empty-state object instead.
Interview Questions on This Topic
- QWhy would you use ArrayDeque instead of the Stack class for implementing a stack in Java, and what specific problems does the legacy Stack class have?
- QExplain the difference between offer/poll/peek and add/remove/element on the Queue interface — when would you deliberately choose the exception-throwing variants?
- QIf you need to implement a thread-safe producer-consumer pipeline in Java, which Queue implementation would you choose and why — and how does BlockingQueue's take() method simplify consumer thread logic compared to polling in a loop?
Frequently Asked Questions
Should I use Stack or ArrayDeque for a stack in Java?
Always use ArrayDeque for new code. Declare it as Deque
What is the difference between poll() and remove() in Java Queue?
Both remove the head of the queue, but they handle an empty queue differently. poll() returns null if the queue is empty — safe for runtime use. remove() throws NoSuchElementException — useful when an empty queue is a bug you want to surface loudly. Prefer poll() in production logic.
Can I use the same ArrayDeque as both a Stack and a Queue?
Technically yes — ArrayDeque implements Deque which supports both ends. But don't mix push/pop with offer/poll on the same instance in the same context; you'll create confusing, unpredictable ordering. Pick one role per instance and stick to it. Use comments or a wrapper class to make the intent crystal clear.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.