Home Java Java Stack and Queue Explained — How, When, and Why to Use Each

Java Stack and Queue Explained — How, When, and Why to Use Each

In Plain English 🔥
Imagine a stack of pancakes — you always add a new pancake on top and always eat from the top. That's a Stack: last in, first out. Now picture a movie ticket line — the first person who queued up is the first one who gets served. That's a Queue: first in, first out. Java gives you both data structures in its Collections framework so you can model exactly these kinds of ordered, disciplined access patterns in your code.
⚡ Quick Answer
Imagine a stack of pancakes — you always add a new pancake on top and always eat from the top. That's a Stack: last in, first out. Now picture a movie ticket line — the first person who queued up is the first one who gets served. That's a Queue: first in, first out. Java gives you both data structures in its Collections framework so you can model exactly these kinds of ordered, disciplined access patterns in your code.

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.

BrowserHistory.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041
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
    }
}
▶ Output
Current page: https://thecodeforge.io/java-collections
Left: https://thecodeforge.io/java-collections
Now on: https://thecodeforge.io
Now on: https://google.com
History empty: true
Peek on empty ArrayDeque: null
⚠️
Watch Out: Legacy Stack vs ArrayDequeNever declare your variable as Stack stack = new Stack<>() in new code. Stack extends Vector, exposing index-based methods that silently break LIFO order. Declare it as Deque stack = new ArrayDeque<>() and you get a true stack with no dangerous backdoors.

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.

TaskScheduler.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243
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
    }
}
▶ Output
Tickets waiting: 3
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
⚠️
Pro Tip: Program to the InterfaceAlways declare your variable as Queue or Deque, never as ArrayDeque directly. This lets you swap LinkedList for ArrayDeque (or LinkedBlockingQueue for thread safety) by changing a single line — the rest of your code stays untouched.

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'.

UndoRedoEditor.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
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);
    }
}
▶ Output
Did: Typed: Hello
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]
🔥
Interview Gold: Why ArrayDeque Over Stack?If an interviewer asks 'What's wrong with Java's Stack class?', your answer is: it extends Vector (thread-synchronized legacy class), which exposes index-based access that violates LIFO semantics. The correct modern replacement is ArrayDeque, which is faster, unsynchronized, and enforces proper stack behaviour through its API surface.
Feature / AspectStack (via ArrayDeque)Queue (via ArrayDeque)
OrderLIFO — Last In, First OutFIFO — First In, First Out
Add elementpush(e) — adds to head/topoffer(e) — adds to tail
Remove elementpop() — removes from head/toppoll() — removes from head
Inspect without removingpeek() — views head/toppeek() — views head
Empty collection behaviourpeek() returns null, pop() throwspeek() returns null, poll() returns null
Backing class (recommended)ArrayDequeArrayDeque or LinkedBlockingQueue (threaded)
Legacy alternative (avoid)java.util.Stack (extends Vector)LinkedList (slower, allows nulls)
Thread-safe variantConcurrentLinkedDequeLinkedBlockingQueue / ArrayBlockingQueue
Real-world use caseUndo/redo, expression parsing, DFSTask 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 deque = new ArrayDeque<>() and use push/pop/peek. The legacy Stack class extends the synchronised Vector, which adds unnecessary overhead and exposes methods that can corrupt LIFO ordering.

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.

🔥
TheCodeForge Editorial Team Verified Author

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.

← PreviousTreeMap and TreeSet in JavaNext →Iterator and ListIterator in Java
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged