Java Tutorial
🔍

Java Queue

Java Queue

java.util.Queue<E> is the interface behind every waiting line in Java — task schedulers, BFS graph traversal, print spoolers, rate limiters, and producer-consumer pipelines all model themselves on exactly this contract. Elements enter at the tail and leave from the head. The element that waited longest is always served first.

Three implementations cover every production scenario: ArrayDeque for fast FIFO queues with no per-element allocation overhead, LinkedList when List and Queue interfaces are needed on the same object, and PriorityQueue when processing order depends on urgency rather than arrival sequence.

What Is the Java Queue Interface?

Queue<E> is an interface in java.util that extends Collection<E>. It adds six methods on top of the Collection contract — three that throw exceptions on failure and three that return a sentinel value (null or false). Every standard implementation honours this contract.

QUEUE HIERARCHY:

java.lang.Iterable<E>
    └── java.util.Collection<E>
            └── java.util.Queue<E>            ← FIFO: tail-in, head-out
                    ├── java.util.Deque<E>    ← extends Queue; adds both-end access
                    │       ├── ArrayDeque    ← circular resizable array (RECOMMENDED)
                    │       └── LinkedList    ← doubly-linked nodes (also implements List)
                    ├── java.util.PriorityQueue   ← binary min-heap; priority order, NOT FIFO
                    └── java.util.concurrent.*
                            ├── LinkedBlockingQueue   ← bounded/unbounded, blocking
                            ├── ArrayBlockingQueue    ← bounded, blocking
                            └── ConcurrentLinkedQueue ← unbounded, non-blocking

KEY FACTS:
  Package    : java.util
  Since      : Java 1.5
  Extends    : Collection<E>
  Extended by: Deque<E>, BlockingQueue<E>
  Null       : strongly discouraged — poll() returns null for EMPTY queue;
               null elements make "empty?" and "null element?" indistinguishable
  Thread-safe: NO for ArrayDeque / LinkedList / PriorityQueue
               YES for ConcurrentLinkedQueue, LinkedBlockingQueue, ArrayBlockingQueue

The Six Method Pairs — Basic Overview

Every Queue operation comes in two flavours that differ only in how they signal failure.

 OPERATION        THROWS on failure               RETURNS sentinel on failure
 ---------------  ------------------------------  --------------------------------
 INSERT at tail   add(e)                          offer(e)
                  throws IllegalStateException    returns false
                  (capacity-bounded queues)       (never on unbounded queues)

 REMOVE from head remove()                        poll()
                  throws NoSuchElementException   returns null
                  (when queue is empty)           (when queue is empty)

 INSPECT head     element()                       peek()
                  throws NoSuchElementException   returns null
                  (when queue is empty)           (when queue is empty)

PRODUCTION RULE:
  Always use the null/false-returning group: offer(), poll(), peek()
  An empty queue is a normal operating condition in most processing loops.
  Wrapping remove() or element() in try-catch for NoSuchElementException
  is never the correct pattern for routine processing.

  Use add(), remove(), element() ONLY when empty or full is a genuine
  programming error that should immediately surface as an exception.

FIFO Visualised

QUEUE STATE AFTER: offer("A"), offer("B"), offer("C")

  TAIL                                   HEAD
   ↓                                      ↓
  [ C ] ← [ B ] ← [ A ]     poll() returns "A" first

  offer("D"):  [ D ] ← [ C ] ← [ B ] ← [ A ]
  poll():      returns "A"  →  [ D ] ← [ C ] ← [ B ]
  poll():      returns "B"  →  [ D ] ← [ C ]
  peek():      returns "C"  →  [ D ] ← [ C ]  (unchanged)

  First in, first out. Insertion order drives removal order.

When to Use Queue

Choosing the right Queue implementation — or whether to use a Queue at all — is what interviewers test when they ask about this topic.

USE ArrayDeque (as Queue) WHEN:
  - FIFO processing in single-threaded code is needed
  - Each element should be processed in arrival order
  - No per-element Node allocation overhead is desired
  - Also suitable as a stack (LIFO) via push()/pop()

USE PriorityQueue WHEN:
  - Processing order depends on urgency, not arrival time
  - "Always serve the most critical item next"
  - Dijkstra's shortest path, A* search, incident management
  - Min-heap natural order: smallest element polled first
  - Custom Comparator: highest-priority polled first

USE LinkedBlockingQueue or ArrayBlockingQueue WHEN:
  - Multiple producer threads + multiple consumer threads
  - Consumers should block when queue is empty (take() blocks)
  - Producers should block when queue is full (bounded, put() blocks)
  - This is the standard Java producer-consumer pattern

USE LinkedList (as Queue) ONLY WHEN:
  - Both the Queue interface AND List index access (get(i)) are needed simultaneously
  - For pure queue operations, ArrayDeque is faster

DO NOT USE Queue WHEN:
  - Random access by index is required — use a List
  - Membership testing is frequent — use a Set or Map
  - Sorted iteration with range queries is needed — use a TreeSet

How Queue Implementations Work Internally

ArrayDeque — Circular Resizable Array

ArrayDeque is the recommended general-purpose Queue. It uses a resizable array with two integer indices — head (next to poll) and tail (next insertion slot) — that wrap around the array boundary using bitwise AND, not modulo.

ARRAYDEQUE INTERNAL STRUCTURE (initial capacity 16):

  After offer("A"), offer("B"), offer("C"), offer("D"):

  elements: [ "A" | "B" | "C" | "D" | null | null | null | null | ... ]
               0     1     2     3     4      5      6      7
  head = 0   (next poll target)
  tail = 4   (next offer slot)

  poll() — removes head:
    result     = elements[0] = "A"
    elements[0] = null         (help GC)
    head       = (0+1) & 15  = 1
    returns "A"

  offer("E"):
    elements[4] = "E"
    tail = (4+1) & 15 = 5

  WRAP-AROUND (head at 14, tail has looped back to 2):
  elements: [ "J" | "K" | null | "C" | "D" | "E" | "F" | "G" | "H" | "I" | ... ]
               0     1     2     3     4     5     6     7     8     9
  head = 3, tail = 2   ← tail is BEHIND head — perfectly valid in circular buffer

  RESIZE when size == capacity:
    New array with double capacity.
    Copy elements from head → end, then 0 → tail, into new[0..size-1].
    head = 0, tail = old size.

WHY ARRAYDEQUE BEATS LINKEDLIST FOR QUEUES:
  - No Node object per element   → lower GC pressure
  - Contiguous array memory      → CPU cache prefetching works
  - Bitwise wrap (& mask)        → faster than modulo (%)
  - Java documentation explicitly recommends ArrayDeque over LinkedList

PriorityQueue — Binary Min-Heap

PriorityQueue breaks FIFO. Every poll() returns the smallest element — or the highest-priority by a custom Comparator. Internally it is a complete binary tree stored in a flat array.

PRIORITYQUEUE BINARY MIN-HEAP (6 elements):

  Elements offered: 40, 15, 60, 25, 10, 50

  HEAP ARRAY after all insertions:
    index: [ 0 | 1 | 2 | 3 | 4 | 5 ]
    value: [10 |15 |50 |25 |40 |60 ]

  TREE REPRESENTATION:
               10          ← always the minimum at root (index 0)
             /    \
           15      50
          /  \    /
         25  40  60

  Parent-child index relationships:
    parent(i)       = (i - 1) / 2
    left_child(i)   = 2*i + 1
    right_child(i)  = 2*i + 2

  peek():
    return elements[0]  →  10   O(1) — root is always the minimum

  poll():   removes root, replaces with last, sifts down  →  O(log n)
    Step 1: save result = 10
    Step 2: elements[0] = elements[5] = 60, size--
    Step 3: sift down 60: compare with children 15 and 50
    Step 4: 15 is smaller → swap 60 and 15
    Step 5: 60 now at index 1: compare with children 25 and 40
    Step 6: 25 is smaller → swap 60 and 25
    Step 7: heap property restored → [15, 25, 50, 60, 40]
    returns 10

  offer(5):   adds to end, sifts up  →  O(log n)
    Step 1: elements[size] = 5
    Step 2: sift up: 5 < parent 50 → swap; 5 < parent 10 → swap
    Step 3: 5 is now root → heap property restored

  CRITICAL: PriorityQueue iterator (for-each) traverses heap ARRAY ORDER.
            This is NOT priority order. Only repeated poll() gives sorted output.

Core Operations with Examples

offer(), poll(), peek() — The Essential Trio

1// File: QueueBasicsDemo.java 2 3import java.util.ArrayDeque; 4import java.util.Queue; 5 6public class QueueBasicsDemo { 7 8 public static void main(String[] args) { 9 10 Queue<String> printJobs = new ArrayDeque<>(); 11 12 System.out.println("=== Enqueue with offer() ==="); 13 System.out.println("offer(Report.pdf) : " + printJobs.offer("Report.pdf")); 14 System.out.println("offer(Invoice.pdf) : " + printJobs.offer("Invoice.pdf")); 15 System.out.println("offer(Payslip.pdf) : " + printJobs.offer("Payslip.pdf")); 16 System.out.println("offer(Contract.pdf) : " + printJobs.offer("Contract.pdf")); 17 System.out.println("Queue : " + printJobs); 18 System.out.println("Size : " + printJobs.size()); 19 20 System.out.println(); 21 22 // peek() — inspect head without removing 23 System.out.println("=== peek() — reads head, queue unchanged ==="); 24 System.out.println("peek() : " + printJobs.peek()); 25 System.out.println("Queue : " + printJobs); // unchanged 26 27 System.out.println(); 28 29 // poll() — FIFO removal — first offered, first returned 30 System.out.println("=== poll() — FIFO dequeue ==="); 31 System.out.println("poll() : " + printJobs.poll()); // Report.pdf — first in 32 System.out.println("poll() : " + printJobs.poll()); // Invoice.pdf 33 System.out.println("Queue : " + printJobs); 34 35 System.out.println(); 36 37 // Behaviour on empty queue — no exceptions with the safe methods 38 System.out.println("=== Safe behaviour on empty queue ==="); 39 printJobs.poll(); printJobs.poll(); // drain remaining 40 System.out.println("isEmpty() : " + printJobs.isEmpty()); 41 System.out.println("poll() empty : " + printJobs.poll()); // null — no exception 42 System.out.println("peek() empty : " + printJobs.peek()); // null — no exception 43 44 // Throwing methods for contrast 45 try { 46 printJobs.remove(); // throws NoSuchElementException 47 } catch (java.util.NoSuchElementException e) { 48 System.out.println("remove() empty : throws NoSuchElementException"); 49 } 50 } 51}
Output:
=== Enqueue with offer() ===
offer(Report.pdf)    : true
offer(Invoice.pdf)   : true
offer(Payslip.pdf)   : true
offer(Contract.pdf)  : true
Queue                : [Report.pdf, Invoice.pdf, Payslip.pdf, Contract.pdf]
Size                 : 4

=== peek() — reads head, queue unchanged ===
peek()  : Report.pdf
Queue   : [Report.pdf, Invoice.pdf, Payslip.pdf, Contract.pdf]

=== poll() — FIFO dequeue ===
poll()  : Report.pdf
poll()  : Invoice.pdf
Queue   : [Payslip.pdf, Contract.pdf]

=== Safe behaviour on empty queue ===
isEmpty()      : true
poll() empty   : null
peek() empty   : null
remove() empty : throws NoSuchElementException

PriorityQueue — Priority Order vs Insertion Order

1// File: PriorityQueueDemo.java 2 3import java.util.Comparator; 4import java.util.PriorityQueue; 5import java.util.Queue; 6 7public class PriorityQueueDemo { 8 9 public static void main(String[] args) { 10 11 // Natural ordering: Integer min-heap — smallest polled first 12 Queue<Integer> ticketNumbers = new PriorityQueue<>(); 13 ticketNumbers.offer(47); ticketNumbers.offer(12); 14 ticketNumbers.offer(83); ticketNumbers.offer(5); 15 ticketNumbers.offer(34); ticketNumbers.offer(61); 16 17 System.out.println("=== Natural ordering — min first ==="); 18 System.out.println("Inserted: [47, 12, 83, 5, 34, 61]"); 19 System.out.println("peek() : " + ticketNumbers.peek()); // 5 — minimum 20 System.out.println("Heap array (NOT sorted): " + ticketNumbers); 21 System.out.print ("poll() order: "); 22 while (!ticketNumbers.isEmpty()) { 23 System.out.print(ticketNumbers.poll() + " "); // 5 12 34 47 61 83 24 } 25 System.out.println(); 26 27 System.out.println(); 28 29 // Custom Comparator — support ticket urgency (higher number = more urgent) 30 record SupportTicket(String id, int urgency, String summary) {} 31 32 Queue<SupportTicket> helpDesk = new PriorityQueue<>( 33 Comparator.comparingInt(SupportTicket::urgency).reversed() 34 ); 35 helpDesk.offer(new SupportTicket("TKT-001", 2, "Password reset request")); 36 helpDesk.offer(new SupportTicket("TKT-002", 5, "Production API is down")); 37 helpDesk.offer(new SupportTicket("TKT-003", 1, "Font rendering issue")); 38 helpDesk.offer(new SupportTicket("TKT-004", 4, "Checkout timeout for all users")); 39 helpDesk.offer(new SupportTicket("TKT-005", 3, "Slow dashboard load")); 40 41 System.out.println("=== Custom order — highest urgency first ==="); 42 System.out.printf("%-10s %4s %s%n", "Ticket", "Urg", "Summary"); 43 System.out.println("-".repeat(55)); 44 while (!helpDesk.isEmpty()) { 45 SupportTicket t = helpDesk.poll(); // always highest urgency next 46 System.out.printf("%-10s %4d %s%n", t.id(), t.urgency(), t.summary()); 47 } 48 } 49}
Output:
=== Natural ordering — min first ===
Inserted: [47, 12, 83, 5, 34, 61]
peek()   : 5
Heap array (NOT sorted): [5, 12, 61, 47, 34, 83]
poll() order: 5 12 34 47 61 83

=== Custom order — highest urgency first ===
Ticket       Urg  Summary
-------------------------------------------------------
TKT-002        5  Production API is down
TKT-004        4  Checkout timeout for all users
TKT-005        3  Slow dashboard load
TKT-001        2  Password reset request
TKT-003        1  Font rendering issue

BFS Graph Traversal — The Canonical Queue Algorithm

Breadth-first search requires a Queue because FIFO guarantees nodes are visited level by level — all distance-1 neighbours before any distance-2 neighbour. Swapping Queue for a Deque-as-stack turns BFS into DFS.

1// File: BFSDemo.java 2 3import java.util.ArrayDeque; 4import java.util.ArrayList; 5import java.util.HashMap; 6import java.util.HashSet; 7import java.util.List; 8import java.util.Map; 9import java.util.Queue; 10import java.util.Set; 11 12public class BFSDemo { 13 14 static List<String> bfs(Map<String, List<String>> graph, String start) { 15 List<String> visitOrder = new ArrayList<>(); 16 Set<String> visited = new HashSet<>(); 17 Queue<String> frontier = new ArrayDeque<>(); // FIFO = level-by-level 18 19 frontier.offer(start); 20 visited.add(start); 21 22 while (!frontier.isEmpty()) { 23 String city = frontier.poll(); // always the oldest-enqueued node 24 visitOrder.add(city); 25 26 for (String neighbour : graph.getOrDefault(city, List.of())) { 27 if (visited.add(neighbour)) { // add returns false if already present 28 frontier.offer(neighbour); 29 } 30 } 31 } 32 return visitOrder; 33 } 34 35 public static void main(String[] args) { 36 37 Map<String, List<String>> metro = new HashMap<>(); 38 metro.put("Mumbai", List.of("Pune", "Nashik", "Surat")); 39 metro.put("Pune", List.of("Mumbai", "Hyderabad", "Bengaluru")); 40 metro.put("Nashik", List.of("Mumbai", "Aurangabad")); 41 metro.put("Surat", List.of("Mumbai", "Ahmedabad")); 42 metro.put("Hyderabad", List.of("Pune", "Chennai")); 43 metro.put("Bengaluru", List.of("Pune", "Chennai")); 44 metro.put("Aurangabad",List.of("Nashik")); 45 metro.put("Ahmedabad", List.of("Surat")); 46 metro.put("Chennai", List.of("Hyderabad", "Bengaluru")); 47 48 System.out.println("BFS from Mumbai (city connectivity):"); 49 System.out.println(bfs(metro, "Mumbai")); 50 System.out.println(); 51 System.out.println("Level 0 (distance 0): Mumbai"); 52 System.out.println("Level 1 (distance 1): Pune, Nashik, Surat"); 53 System.out.println("Level 2 (distance 2): Hyderabad, Bengaluru, Aurangabad, Ahmedabad"); 54 System.out.println("Level 3 (distance 3): Chennai"); 55 } 56}
Output:
BFS from Mumbai (city connectivity):
[Mumbai, Pune, Nashik, Surat, Hyderabad, Bengaluru, Aurangabad, Ahmedabad, Chennai]

Level 0 (distance 0): Mumbai
Level 1 (distance 1): Pune, Nashik, Surat
Level 2 (distance 2): Hyderabad, Bengaluru, Aurangabad, Ahmedabad
Level 3 (distance 3): Chennai

Real-World Example — Swiggy Order Dispatch System

A Swiggy dispatch system processes delivery orders in arrival order for standard deliveries. Express orders skip the regular queue — they enter a PriorityQueue that always dispatches the most recently received express order first among all express orders. The dispatcher checks the express queue before the standard queue on every dispatch cycle.

1// File: DeliveryOrder.java 2 3public class DeliveryOrder implements Comparable<DeliveryOrder> { 4 5 private final String orderId; 6 private final String restaurant; 7 private final String customer; 8 private final String tier; // "EXPRESS" or "STANDARD" 9 private final double amount; 10 private final long receivedAt; // millisecond timestamp for tiebreaking 11 12 public DeliveryOrder(String orderId, String restaurant, 13 String customer, String tier, double amount) { 14 this.orderId = orderId; 15 this.restaurant = restaurant; 16 this.customer = customer; 17 this.tier = tier; 18 this.amount = amount; 19 this.receivedAt = System.nanoTime(); // preserve arrival sequence 20 } 21 22 public String getTier() { return tier; } 23 public String getOrderId() { return orderId; } 24 25 // Express orders: lower receivedAt = earlier arrival = higher priority 26 @Override 27 public int compareTo(DeliveryOrder other) { 28 return Long.compare(this.receivedAt, other.receivedAt); 29 } 30 31 @Override 32 public String toString() { 33 return String.format("[%s] %-16s → %-10s Rs.%6.2f (%s)", 34 orderId, restaurant, customer, amount, tier); 35 } 36}
1// File: DispatchCoordinator.java 2 3import java.util.ArrayDeque; 4import java.util.PriorityQueue; 5import java.util.Queue; 6 7public class DispatchCoordinator { 8 9 // Express: priority queue — earliest-received express order dispatched first 10 private final Queue<DeliveryOrder> expressLane = new PriorityQueue<>(); 11 // Standard: FIFO queue — first ordered, first dispatched 12 private final Queue<DeliveryOrder> standardLane = new ArrayDeque<>(); 13 14 public void receive(DeliveryOrder order) { 15 if ("EXPRESS".equals(order.getTier())) { 16 expressLane.offer(order); 17 } else { 18 standardLane.offer(order); 19 } 20 System.out.printf(" RECEIVED [exp=%d std=%d]: %s%n", 21 expressLane.size(), standardLane.size(), order); 22 } 23 24 public DeliveryOrder dispatchNext() { 25 // Express orders always take priority over standard 26 Queue<DeliveryOrder> source = !expressLane.isEmpty() ? expressLane : standardLane; 27 DeliveryOrder order = source.poll(); 28 if (order != null) { 29 System.out.println(" DISPATCH : " + order); 30 } else { 31 System.out.println(" DISPATCH : no orders pending"); 32 } 33 return order; 34 } 35 36 public void status() { 37 System.out.printf(" QUEUE STATUS → express: %d standard: %d%n", 38 expressLane.size(), standardLane.size()); 39 } 40 41 public static void main(String[] args) throws InterruptedException { 42 43 DispatchCoordinator coordinator = new DispatchCoordinator(); 44 45 System.out.println("--- Orders arriving ---"); 46 coordinator.receive(new DeliveryOrder("ORD-001","Dominos", "Priya", "STANDARD", 549.0)); 47 coordinator.receive(new DeliveryOrder("ORD-002","Burger King", "Rohan", "STANDARD", 389.0)); 48 Thread.sleep(1); // ensure measurable nano difference 49 coordinator.receive(new DeliveryOrder("ORD-003","KFC", "Ananya", "EXPRESS", 749.0)); 50 Thread.sleep(1); 51 coordinator.receive(new DeliveryOrder("ORD-004","Pizza Hut", "Karan", "STANDARD", 899.0)); 52 Thread.sleep(1); 53 coordinator.receive(new DeliveryOrder("ORD-005","McDonald's", "Divya", "EXPRESS", 299.0)); 54 55 System.out.println(); 56 coordinator.status(); 57 58 System.out.println("\n--- Dispatching all orders ---"); 59 for (int i = 0; i < 5; i++) { 60 coordinator.dispatchNext(); 61 } 62 coordinator.dispatchNext(); // one extra — should report empty 63 64 System.out.println(); 65 coordinator.status(); 66 } 67}
Output:
--- Orders arriving ---
  RECEIVED  [exp=0 std=1]: [ORD-001] Dominos          → Priya       Rs.549.00  (STANDARD)
  RECEIVED  [exp=0 std=2]: [ORD-002] Burger King      → Rohan       Rs.389.00  (STANDARD)
  RECEIVED  [exp=1 std=2]: [ORD-003] KFC              → Ananya      Rs.749.00  (EXPRESS)
  RECEIVED  [exp=1 std=3]: [ORD-004] Pizza Hut        → Karan       Rs.899.00  (STANDARD)
  RECEIVED  [exp=2 std=3]: [ORD-005] McDonald's       → Divya       Rs.299.00  (EXPRESS)

  QUEUE STATUS → express: 2  standard: 3

--- Dispatching all orders ---
  DISPATCH  : [ORD-003] KFC              → Ananya      Rs.749.00  (EXPRESS)
  DISPATCH  : [ORD-005] McDonald's       → Divya       Rs.299.00  (EXPRESS)
  DISPATCH  : [ORD-001] Dominos          → Priya       Rs.549.00  (STANDARD)
  DISPATCH  : [ORD-002] Burger King      → Rohan       Rs.389.00  (STANDARD)
  DISPATCH  : [ORD-004] Pizza Hut        → Karan       Rs.899.00  (STANDARD)
  DISPATCH  : no orders pending

  QUEUE STATUS → express: 0  standard: 0

Performance Considerations

OperationArrayDequeLinkedListPriorityQueue
offer(e)O(1) amortisedO(1)O(log n)
poll()O(1) amortisedO(1)O(log n)
peek()O(1)O(1)O(1)
contains(e)O(n)O(n)O(n)
size()O(1)O(1)O(1)
ResizeO(n) amortisedN/AO(n) amortised
Memory/element~4-8 bytes~28 bytes (Node)~4-8 bytes
Iteration orderFIFO (insertion)FIFO (insertion)Heap array — NOT priority order

ArrayDeque vs LinkedList: ArrayDeque wins on every axis for pure queue use. No per-element Node allocation means far less GC pressure. Contiguous array memory means CPU caches stay warm. The Java documentation explicitly recommends ArrayDeque over LinkedList for both queue and stack use. LinkedList earns its place only when List-interface methods (get(index), set(index, value)) are needed alongside queue operations.

PriorityQueue heap cost: Every offer() and poll() triggers a sift-up or sift-down operation — O(log n) guaranteed. For n elements, building the heap by inserting one at a time is O(n log n). The PriorityQueue(Collection) constructor uses Floyd's algorithm to build the heap in O(n) — prefer it when initialising from an existing collection.

Thread safety: None of the three main implementations are thread-safe. For concurrent queues: LinkedBlockingQueue for blocking producer-consumer, ArrayBlockingQueue for bounded blocking, ConcurrentLinkedQueue for non-blocking high-throughput. Never share ArrayDeque or PriorityQueue across threads without external synchronisation.

Best Practices

Declare the variable type as Queue<E>, not the concrete implementation. Queue<String> tasks = new ArrayDeque<>() lets you change to LinkedBlockingQueue for thread safety or PriorityQueue for prioritisation by changing one line. ArrayDeque<String> tasks = new ArrayDeque<>() locks every method signature to one class unnecessarily. The sole exception: use PriorityQueue<E> directly when navigation methods beyond the Queue interface are needed.

Always prefer offer(), poll(), and peek() over add(), remove(), and element(). An empty queue is a routine operational condition — the processing loop has simply drained its input. Throwing NoSuchElementException for an empty queue and catching it in a loop is the wrong pattern. String next; while ((next = queue.poll()) != null) is the clean idiom.

Never insert null into a Queue. poll() returning null signals "queue is empty." If null elements were stored, poll() returning null is ambiguous — it could mean empty or it could mean the head was null. Both ArrayDeque and PriorityQueue throw NullPointerException on offer(null). If a "no value" sentinel is needed, use Optional<E> as the element type or a dedicated sentinel constant.

For PriorityQueue, always use poll() to process in priority order — never for-each. The for-each iterator on PriorityQueue traverses the internal heap array, which is not sorted. Only the root (index 0) is guaranteed to be the minimum. Processing in priority order requires draining with poll(). If you need to inspect all elements in priority order while keeping the queue intact, copy it first: new PriorityQueue<>(original).

Common Mistakes

Mistake 1 — Iterating PriorityQueue and Expecting Priority Order

1PriorityQueue<Integer> pq = new PriorityQueue<>(); 2pq.offer(30); pq.offer(10); pq.offer(20); pq.offer(5); pq.offer(40); 3 4// WRONG — for-each traverses heap array; result is NOT [5, 10, 20, 30, 40] 5System.out.println("for-each: " + pq); // [5, 10, 20, 30, 40] — happens to look sorted here 6// but for other inputs it will NOT be sorted — heap array != sorted array 7 8// CORRECT — repeated poll() guarantees ascending (priority) order 9System.out.print("poll() order: "); 10while (!pq.isEmpty()) { 11 System.out.print(pq.poll() + " "); // 5 10 20 30 40 — guaranteed 12} 13System.out.println();
Output:
for-each: [5, 10, 20, 30, 40]
poll() order: 5 10 20 30 40

Mistake 2 — Inserting null Into ArrayDeque

1Queue<String> events = new ArrayDeque<>(); 2events.offer("LoginEvent"); 3 4// WRONG — ArrayDeque throws NullPointerException on offer(null) 5// events.offer(null); // NullPointerException at runtime 6 7// CORRECT — use a sentinel value or Optional to represent absence 8events.offer("SHUTDOWN_SIGNAL"); // explicit sentinel for consumer shutdown 9// Or use Optional<String> as the element type: 10Queue<java.util.Optional<String>> safeEvents = new ArrayDeque<>(); 11safeEvents.offer(java.util.Optional.of("LoginEvent")); 12safeEvents.offer(java.util.Optional.empty()); // represents "end of stream"

Mistake 3 — Using LinkedList When ArrayDeque Is Sufficient

1// WRONG — every element allocates a Node; higher GC pressure; slower 2Queue<String> requestQueue = new LinkedList<>(); 3 4// CORRECT — no Node allocation, cache-friendly, explicitly recommended by Java docs 5Queue<String> requestQueue2 = new ArrayDeque<>(); 6 7// LinkedList as Queue is justified ONLY when you need this simultaneously: 8// list.get(2), list.set(0, value), list.subList(1, 4) 9// alongside queue operations on the same object.

Mistake 4 — Forgetting That PriorityQueue Needs a Tiebreaker

1record Task(String id, int priority) {} 2 3// WRONG — two tasks with the same priority: compare returns 0 for different ids 4// PriorityQueue does not guarantee which of the two is polled first 5PriorityQueue<Task> tasks = new PriorityQueue<>( 6 Comparator.comparingInt(Task::priority) 7); 8tasks.offer(new Task("T1", 3)); 9tasks.offer(new Task("T2", 3)); // same priority — relative order undefined 10 11// CORRECT — add a tiebreaker to enforce stable ordering within same priority 12PriorityQueue<Task> stableTasks = new PriorityQueue<>( 13 Comparator.comparingInt(Task::priority) 14 .thenComparing(Task::id) // tiebreaker: alphabetical id order 15);

Interview Questions

Q1. What is the Queue interface in Java and what does FIFO mean?

Queue<E> is an interface in java.util that extends Collection and models a First-In, First-Out container. FIFO means the element inserted earliest is always removed first — like a physical queue at a service counter. The interface defines six methods in two parallel groups: add/offer for insertion, remove/poll for head-retrieval-and-removal, element/peek for head-inspection. The null-returning group (offer, poll, peek) is preferred for routine processing code, reserving the exception-throwing group for cases where an empty queue represents a programming error.

Q2. What is the difference between offer() and add() in Queue?

Both insert an element at the tail. add() throws IllegalStateException if the queue has a capacity limit and is full. offer() returns false instead. For unbounded queues like ArrayDeque and LinkedList, neither ever reaches capacity — both behave identically. For bounded queues like ArrayBlockingQueue, the distinction matters. In production code, offer() is the standard choice because it expresses intent clearly: "attempt to insert, handle failure explicitly." add() is appropriate when insertion failure is a genuine programming error that should propagate immediately.

Q3. Why is ArrayDeque preferred over LinkedList as a Queue implementation?

ArrayDeque uses a resizable circular array — elements are stored contiguously, which is friendly to CPU caches. No per-element Node object is allocated; every offer() simply writes to an array slot, and every poll() nulls an array slot and advances an index. LinkedList allocates a new Node object per element — higher GC pressure, scattered heap allocations, and cache misses on every node traversal. The Java documentation explicitly recommends ArrayDeque over LinkedList for both queue and stack use cases. LinkedList as a queue is only justified when List interface methods (index-based get, set, subList) are also needed on the same object.

Q4. How does PriorityQueue differ from a regular FIFO Queue?

PriorityQueue ignores insertion order. Every poll() returns the element with the highest priority — for natural ordering, the smallest element; for a custom Comparator, whichever the comparator ranks first. Internally it uses a binary min-heap stored in a flat array, where the root is always the minimum. offer() and poll() are O(log n) due to sift-up and sift-down operations. Unlike ArrayDeque, PriorityQueue iteration does not traverse in priority order — the heap array is not sorted. Only repeated poll() calls consume elements in priority order.

Q5. How is Queue used in BFS graph traversal?

BFS explores a graph layer by layer — all nodes at distance 1 from the source before any at distance 2. A Queue enforces this because FIFO means nodes are processed in the exact order they were discovered: the source is enqueued first, its unvisited neighbours are enqueued next, and when those are processed their unvisited neighbours join the tail. Every poll() always returns the longest-waiting node, which is the closest to the source among all remaining nodes. Replacing Queue with a stack (LIFO) changes the traversal to DFS.

Q6. How do you implement a thread-safe Queue in Java?

Three standard options from java.util.concurrent. LinkedBlockingQueue — optionally bounded; take() blocks consumers when empty, put() blocks producers when full (if bounded); the standard choice for producer-consumer pipelines. ArrayBlockingQueue — bounded with a fixed array; slightly more memory-efficient than LinkedBlockingQueue when capacity is fixed. ConcurrentLinkedQueue — unbounded, non-blocking; offer() and poll() use CAS operations and never block; weakly consistent iteration; preferred for high-throughput scenarios where blocking is unacceptable. Never use Collections.synchronizedCollection() on a Queue — it synchronises individual calls but not compound operations like check-then-act patterns.

FAQs

What is the default Queue implementation in Java?

There is no default — Queue is an interface. The Java documentation recommends ArrayDeque as the general-purpose implementation for all single-threaded queue and stack use cases. For priority-based processing use PriorityQueue. For blocking multi-threaded queues use LinkedBlockingQueue or ArrayBlockingQueue from java.util.concurrent.

What is the difference between Queue and Deque in Java?

Queue is a single-ended interface — elements enter at the tail and leave from the head. Deque (Double-Ended Queue) extends Queue and adds symmetric operations on both ends: addFirst/addLast, removeFirst/removeLast, peekFirst/peekLast. A Deque can serve as a FIFO queue (tail-in, head-out) or a LIFO stack (head-in, head-out) or a sliding window buffer. ArrayDeque implements Deque and is the recommended implementation for both patterns.

Can PriorityQueue have duplicate elements?

Yes. PriorityQueue allows multiple elements with equal priority — it never rejects duplicates. Unlike TreeSet, PriorityQueue does not treat compare == 0 as "same element." Two elements that compare as equal are both stored and both available for successive poll() calls. Their relative order when both are at the head is not guaranteed unless a tiebreaker is included in the Comparator.

What happens if no Comparator is given to PriorityQueue for a custom class?

The first offer() succeeds because there is nothing to compare against. The second offer() calls compareTo() on the first element to find the heap position. If the element class does not implement Comparable, Java throws ClassCastException at runtime. The compiler does not catch this — generics allow PriorityQueue<MyClass> even without Comparable. Always either implement Comparable on the element class or pass a Comparator to the constructor.

Is PriorityQueue sorted at all times?

No. The internal array satisfies the heap property — every parent is smaller than its children — but the full array is not sorted. toString() and for-each reflect heap-array order, which looks partially sorted but is not guaranteed to be fully sorted. Only the root (index 0) is guaranteed to be the current minimum. Building a sorted view requires draining with poll().

How do you process all elements in a Queue without destroying it?

Iterate with for-each: for (String item : queue) traverses all elements without removing them. For ArrayDeque and LinkedList this traverses in FIFO order. For PriorityQueue this traverses heap-array order, which is NOT priority order. To inspect all elements in priority order while preserving the original queue, copy it: PriorityQueue<T> copy = new PriorityQueue<>(original); while (!copy.isEmpty()) process(copy.poll());.

Summary

Queue<E> models FIFO processing — first offered, first polled. Its six methods pair into two groups: the exception-throwing add/remove/element for when failure is a programming error, and the null-returning offer/poll/peek for routine processing loops. The null-returning trio is the correct default for almost all production code.

ArrayDeque is the right Queue for single-threaded FIFO work — no per-element allocation, cache-friendly, and explicitly recommended over LinkedList by the Java documentation. PriorityQueue breaks FIFO for priority-based dispatch — poll() always returns the heap root, and for-each iteration does not follow priority order. For multi-threaded queues, reach for LinkedBlockingQueue or ArrayBlockingQueue.

For interviews: know the six method pairs and which group to use in which scenario, explain the ArrayDeque circular-array structure and why it beats LinkedList, describe the binary min-heap mechanics of PriorityQueue, demonstrate BFS using a Queue, and explain why PriorityQueue for-each does not iterate in priority order. These cover every level from campus recruitment to senior engineering rounds.

What to Read Next