Java Tutorial
🔍

Java PriorityQueue

Java PriorityQueue

java.util.PriorityQueue<E> breaks the waiting-line contract that every other Queue honours. Elements are not served in arrival order — they are served in priority order. Every poll() call returns the element with the highest priority regardless of when it was inserted. Under the hood this is a binary min-heap: a complete binary tree stored in a flat array where the root is always the smallest element. Understanding the heap is what separates developers who use PriorityQueue confidently from those who accidentally break it.

What Is Java PriorityQueue?

PriorityQueue<E> is a concrete class in java.util that implements Queue<E>. It stores elements in a binary min-heap — a data structure where every parent is smaller than (or equal to) both its children. The practical effect: peek() is always O(1) and returns the current minimum, poll() removes and returns the minimum in O(log n), and insertion via offer() is O(log n).

The diagram below shows where PriorityQueue fits in the Collections hierarchy.

java.lang.Iterable<E>
    └── java.util.Collection<E>
            └── java.util.Queue<E>          ← FIFO contract
                    ├── java.util.Deque<E>
                    │       └── ArrayDeque  ← FIFO/LIFO, arrival order
                    └── java.util.PriorityQueue<E>  ← PRIORITY ORDER (not FIFO)

KEY FACTS:
  Package    : java.util
  Since      : Java 1.5
  Implements : Queue<E>, Collection<E>, Iterable<E>
  Backed by  : binary min-heap in a resizable Object[] array
  Ordering   : natural (Comparable) by default; Comparator-supplied for custom order
  Null       : NOT allowed — compareTo(null) throws NullPointerException
  Duplicates : allowed — equal-priority elements are both stored
  Thread     : NOT thread-safe (use PriorityBlockingQueue for concurrent access)
  Default capacity: 11 elements
  Grow factor : roughly 50% when capacity is exceeded

Basic Overview — The Min-Heap Structure

Every element in a PriorityQueue lives inside a flat Object[] array that represents a complete binary tree. The root is always at index 0 and is always the minimum element.

BINARY MIN-HEAP for [10, 20, 15, 40, 30, 25]:

  ARRAY LAYOUT:
    index:  [ 0  |  1  |  2  |  3  |  4  |  5  ]
    value:  [ 10 | 20  | 15  | 40  | 30  | 25  ]

  TREE VIEW:
              10           ← root = minimum (index 0)
            /    \
          20      15
         /  \    /
        40  30  25

  PARENT-CHILD INDEX RELATIONSHIPS:
    parent(i)     = (i - 1) / 2   (integer division)
    leftChild(i)  = 2 * i + 1
    rightChild(i) = 2 * i + 2

  HEAP PROPERTY:
    array[parent(i)] <= array[i]  for all i > 0
    Every parent is smaller than or equal to both its children.
    This guarantees: array[0] is ALWAYS the minimum element.

  WHAT ITERATION SHOWS (for-each, toString()):
    Traverses the array in index order → NOT sorted order
    [10, 20, 15, 40, 30, 25] — only root is guaranteed to be minimum

When to Use PriorityQueue

The choice between PriorityQueue, ArrayDeque, TreeSet, and LinkedList depends on what "next" means for your use case.

USE PriorityQueue WHEN:
  1. "Always serve the most important element next" regardless of arrival order
     — incident queues (critical bugs before minor bugs)
     — task schedulers (high-priority jobs first)
     — Dijkstra's shortest path (nearest unexplored node first)
     — A* search (best-estimate node first)

  2. Top-K problems: "find the K smallest/largest elements in a dataset"
     — maintain a max-heap of size K → K smallest always inside
     — maintain a min-heap of size K → K largest always inside

  3. Merge K sorted arrays/lists: use a min-heap to always pick the
     smallest current front element across K arrays

  4. Streaming median: maintain two heaps (max for lower half, min for upper half)

CHOOSE ArrayDeque INSTEAD WHEN:
  - Strict FIFO is needed — first received, first processed
  - No priority difference exists between elements

CHOOSE TreeSet INSTEAD WHEN:
  - Sorted unique elements AND range queries (floor, ceiling, headSet) are needed
  - Duplicates must be rejected based on comparison equality

CHOOSE PriorityBlockingQueue INSTEAD WHEN:
  - Multiple producer and consumer threads share the queue
  - PriorityQueue is not thread-safe

DO NOT USE PriorityQueue WHEN:
  - Iteration in sorted order is needed — use TreeSet
  - Random access by index is needed — use ArrayList
  - The order of equal-priority elements must be preserved (FIFO within same priority)
    — PriorityQueue does not guarantee ordering among equal-priority elements

How PriorityQueue Works Internally

Sift-Up — What Happens on offer()

Every offer(element) adds the new element at the end of the array (the next leaf position) and then sifts it up by repeatedly swapping it with its parent until the heap property is restored.

OFFER(8) into existing heap [10, 20, 15, 40, 30, 25]:

  BEFORE:
    array: [10, 20, 15, 40, 30, 25]
    tree:       10
               /  \
             20    15
            / \   /
           40 30 25

  Step 1: append 8 at index 6
    array: [10, 20, 15, 40, 30, 25, 8]
    8 is at index 6, parent = (6-1)/2 = 2, parent value = 15

  Step 2: 8 < 15 → swap 8 and 15
    array: [10, 20, 8, 40, 30, 25, 15]
    8 is now at index 2, parent = (2-1)/2 = 0, parent value = 10

  Step 3: 8 < 10 → swap 8 and 10
    array: [8, 20, 10, 40, 30, 25, 15]
    8 is now at index 0 — it is the root, no parent to check

  AFTER:
    array: [8, 20, 10, 40, 30, 25, 15]
    tree:        8             ← new minimum at root
               /   \
             20     10
            / \    /  \
           40 30  25  15

  Each sift-up comparison moves one level up: O(log n) steps maximum.

Sift-Down — What Happens on poll()

poll() removes the root (minimum element), places the last array element at the root, and sifts it down by repeatedly swapping it with the smaller of its two children until the heap property is restored.

POLL() from heap [8, 20, 10, 40, 30, 25, 15]:

  Step 1: save result = 8 (root)
  Step 2: move last element (15) to root; size--
    array: [15, 20, 10, 40, 30, 25]
    15 is at index 0; children: left=20 (index 1), right=10 (index 2)

  Step 3: 15 > min(20,10)=10 → swap 15 and 10
    array: [10, 20, 15, 40, 30, 25]
    15 is at index 2; children: left=25 (index 5), no right child

  Step 4: 15 < 25 → heap property satisfied, stop

  AFTER:
    array: [10, 20, 15, 40, 30, 25]
    tree:       10
               /  \
             20    15
            / \   /
           40 30 25

  Returns: 8 — the previous minimum.
  Each sift-down comparison moves one level down: O(log n) steps maximum.

Floyd's Heapify — Bulk Construction in O(n)

When you create a PriorityQueue from an existing collection using the PriorityQueue(Collection) constructor, Java does not insert elements one by one. It uses Floyd's heapify algorithm — which builds a valid heap from an arbitrary array in O(n) time, not O(n log n).

FLOYD'S HEAPIFY on arbitrary array [35, 10, 50, 20, 30]:

  Start as a complete binary tree:
         35
        /   \
      10     50
     / \
    20  30

  Process all non-leaf nodes from right to left (index n/2-1 down to 0):
  Last non-leaf: index (5/2 - 1) = 1  (value 10)

  Sift-down index 1 (value 10):
    children: 20 (idx 3), 30 (idx 4)
    10 < min(20,30) → already correct, no swap

  Sift-down index 0 (value 35):
    children: 10 (idx 1), 50 (idx 2)
    35 > min(10,50)=10 → swap 35 and 10
    35 now at index 1: children 20 (idx 3), 30 (idx 4)
    35 > min(20,30)=20 → swap 35 and 20
    35 now at index 3: no children — stop

  RESULT: [10, 20, 50, 35, 30]
  Valid heap — constructed in O(n), not O(n log n)

Core Operations with Examples

offer(), poll(), peek() — Priority Order in Action

1// File: PriorityQueueBasicsDemo.java 2 3import java.util.PriorityQueue; 4 5public class PriorityQueueBasicsDemo { 6 7 public static void main(String[] args) { 8 9 // Default: natural ordering (Integer min-heap — smallest polled first) 10 PriorityQueue<Integer> minHeap = new PriorityQueue<>(); 11 12 // Elements inserted in arbitrary order 13 minHeap.offer(42); 14 minHeap.offer(7); 15 minHeap.offer(91); 16 minHeap.offer(15); 17 minHeap.offer(3); 18 minHeap.offer(56); 19 20 System.out.println("=== Min-heap basics ==="); 21 System.out.println("Inserted : [42, 7, 91, 15, 3, 56]"); 22 System.out.println("Internal array: " + minHeap); // heap array, NOT sorted 23 System.out.println("peek() : " + minHeap.peek()); // 3 — minimum, O(1) 24 System.out.println("size() : " + minHeap.size()); 25 26 System.out.println("\n=== poll() always returns minimum ==="); 27 System.out.print("Processing order: "); 28 while (!minHeap.isEmpty()) { 29 System.out.print(minHeap.poll() + " "); // always smallest remaining 30 } 31 System.out.println(); 32 33 System.out.println(); 34 35 // Max-heap using Comparator.reverseOrder() 36 PriorityQueue<Integer> maxHeap = new PriorityQueue<>(java.util.Comparator.reverseOrder()); 37 maxHeap.offer(42); maxHeap.offer(7); 38 maxHeap.offer(91); maxHeap.offer(15); 39 maxHeap.offer(3); maxHeap.offer(56); 40 41 System.out.println("=== Max-heap with Comparator.reverseOrder() ==="); 42 System.out.println("peek() : " + maxHeap.peek()); // 91 — maximum 43 System.out.print("Processing order: "); 44 while (!maxHeap.isEmpty()) { 45 System.out.print(maxHeap.poll() + " "); // always largest remaining 46 } 47 System.out.println(); 48 } 49}
Output:
=== Min-heap basics ===
Inserted     : [42, 7, 91, 15, 3, 56]
Internal array: [3, 7, 56, 42, 15, 91]
peek()       : 3
size()       : 6

=== poll() always returns minimum ===
Processing order: 3 7 15 42 56 91

=== Max-heap with Comparator.reverseOrder() ===
peek() : 91
Processing order: 91 56 42 15 7 3

Custom Objects — Comparator-Based Priority

1// File: PriorityQueueCustomDemo.java 2 3import java.util.Comparator; 4import java.util.PriorityQueue; 5 6public class PriorityQueueCustomDemo { 7 8 record Task(String id, int priority, String description) {} 9 10 public static void main(String[] args) { 11 12 // Lower priority number = higher urgency (1 = critical, 5 = low) 13 PriorityQueue<Task> taskQueue = new PriorityQueue<>( 14 Comparator.comparingInt(Task::priority) 15 .thenComparing(Task::id) // stable tiebreaker within same priority 16 ); 17 18 taskQueue.offer(new Task("T003", 2, "API rate limit alert")); 19 taskQueue.offer(new Task("T001", 1, "Database is unreachable")); 20 taskQueue.offer(new Task("T005", 3, "Memory usage at 85 percent")); 21 taskQueue.offer(new Task("T002", 1, "Authentication service down")); 22 taskQueue.offer(new Task("T004", 2, "Slow query degrading performance")); 23 24 System.out.println("=== Custom priority — highest urgency first ==="); 25 System.out.printf("%-8s %-4s %s%n", "Task", "Pri", "Description"); 26 System.out.println("-".repeat(55)); 27 while (!taskQueue.isEmpty()) { 28 Task task = taskQueue.poll(); 29 System.out.printf("%-8s %-4d %s%n", 30 task.id(), task.priority(), task.description()); 31 } 32 33 System.out.println(); 34 35 // Bulk construction via Collection — uses Floyd's O(n) heapify 36 java.util.List<Integer> scores = java.util.List.of(88, 45, 92, 67, 78, 55, 95, 33, 71); 37 PriorityQueue<Integer> fromList = new PriorityQueue<>(scores); 38 System.out.println("=== Built from List using Floyd's O(n) heapify ==="); 39 System.out.println("List : " + scores); 40 System.out.println("Heap array : " + fromList); 41 System.out.println("Minimum : " + fromList.peek()); 42 } 43}
Output:
=== Custom priority — highest urgency first ===
Task     Pri  Description
-------------------------------------------------------
T001     1    Database is unreachable
T002     1    Authentication service down
T003     2    API rate limit alert
T004     2    Slow query degrading performance
T005     3    Memory usage at 85 percent

=== Built from List using Floyd's O(n) heapify ===
List       : [88, 45, 92, 67, 78, 55, 95, 33, 71]
Heap array : [33, 45, 55, 67, 78, 92, 95, 88, 71]
Minimum    : 33

Top-K Elements — The Classic PriorityQueue Pattern

Finding the K smallest or K largest elements from a large dataset is one of the most tested PriorityQueue patterns in product company interviews.

1// File: TopKElementsDemo.java 2 3import java.util.ArrayList; 4import java.util.Comparator; 5import java.util.List; 6import java.util.PriorityQueue; 7 8public class TopKElementsDemo { 9 10 // Find K smallest elements using a max-heap of size K 11 // Logic: maintain a max-heap; when size exceeds K, remove the maximum. 12 // The K elements remaining in the heap are the K smallest. 13 static List<Integer> kSmallest(int[] nums, int k) { 14 PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder()); 15 for (int num : nums) { 16 maxHeap.offer(num); 17 if (maxHeap.size() > k) { 18 maxHeap.poll(); // evict the largest — keep only K smallest 19 } 20 } 21 return new ArrayList<>(maxHeap); // heap contents = K smallest 22 } 23 24 // Find K largest elements using a min-heap of size K 25 // Logic: maintain a min-heap; when size exceeds K, remove the minimum. 26 // The K elements remaining are the K largest. 27 static List<Integer> kLargest(int[] nums, int k) { 28 PriorityQueue<Integer> minHeap = new PriorityQueue<>(); 29 for (int num : nums) { 30 minHeap.offer(num); 31 if (minHeap.size() > k) { 32 minHeap.poll(); // evict the smallest — keep only K largest 33 } 34 } 35 return new ArrayList<>(minHeap); 36 } 37 38 // Kth smallest element: same as kSmallest but return heap root (peek) 39 static int kthSmallest(int[] nums, int k) { 40 PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder()); 41 for (int num : nums) { 42 maxHeap.offer(num); 43 if (maxHeap.size() > k) maxHeap.poll(); 44 } 45 return maxHeap.peek(); // root of max-heap of size K = Kth smallest 46 } 47 48 public static void main(String[] args) { 49 50 int[] examScores = {88, 45, 92, 67, 78, 55, 95, 33, 71, 84, 62, 99, 41}; 51 int k = 4; 52 53 System.out.println("Dataset: " + java.util.Arrays.toString(examScores)); 54 System.out.println("K = " + k); 55 System.out.println(); 56 57 List<Integer> smallest = kSmallest(examScores, k); 58 System.out.println(k + " smallest : " + smallest); 59 60 List<Integer> largest = kLargest(examScores, k); 61 System.out.println(k + " largest : " + largest); 62 63 System.out.println(k + "th smallest : " + kthSmallest(examScores, k)); 64 65 System.out.println(); 66 67 // Business context: top-4 discount earners by spend amount 68 record Customer(String name, double spend) {} 69 int[] spends = {12500, 3200, 8900, 45000, 1500, 22000, 6700, 37000, 9800, 15000}; 70 int topK = 3; 71 72 PriorityQueue<Integer> topSpenders = new PriorityQueue<>(); 73 for (int spend : spends) { 74 topSpenders.offer(spend); 75 if (topSpenders.size() > topK) topSpenders.poll(); 76 } 77 System.out.println("Top " + topK + " spend amounts: " + new ArrayList<>(topSpenders)); 78 } 79}
Output:
Dataset: [88, 45, 92, 67, 78, 55, 95, 33, 71, 84, 62, 99, 41]
K = 4

4 smallest : [67, 45, 55, 33]
4 largest  : [92, 95, 88, 99]
4th smallest : 67

Top 3 spend amounts: [22000, 37000, 45000]

Real-World Example — CRED Customer Support Escalation Engine

A customer support system at CRED receives support tickets continuously. Tickets are not processed FIFO — a billing dispute that blocks a Rs.50,000 payment must be processed before a cosmetic UI complaint. The escalation engine uses a PriorityQueue where urgency and age determine dispatch order. Tickets that have been waiting too long are automatically escalated.

1// File: SupportTicket.java 2 3import java.util.Objects; 4 5public class SupportTicket implements Comparable<SupportTicket> { 6 7 private final String ticketId; 8 private final String customerId; 9 private final String category; // BILLING, TECHNICAL, COSMETIC 10 private int urgency; // 1 = critical, 2 = high, 3 = medium, 4 = low 11 private final long createdAtMs; 12 private final String summary; 13 14 public SupportTicket(String ticketId, String customerId, 15 String category, int urgency, String summary) { 16 this.ticketId = ticketId; 17 this.customerId = customerId; 18 this.category = category; 19 this.urgency = urgency; 20 this.createdAtMs = System.currentTimeMillis(); 21 this.summary = summary; 22 } 23 24 public String getTicketId() { return ticketId; } 25 public String getCategory() { return category; } 26 public int getUrgency() { return urgency; } 27 public long getCreatedAtMs() { return createdAtMs; } 28 29 public void escalate() { 30 if (urgency > 1) { 31 urgency--; 32 System.out.println(" ESCALATED: " + ticketId + " → urgency " + urgency); 33 } 34 } 35 36 // Primary: urgency ascending (1 before 4 — critical before low) 37 // Secondary: createdAtMs ascending (older tickets before newer within same urgency) 38 @Override 39 public int compareTo(SupportTicket other) { 40 int cmp = Integer.compare(this.urgency, other.urgency); 41 if (cmp != 0) return cmp; 42 return Long.compare(this.createdAtMs, other.createdAtMs); 43 } 44 45 @Override 46 public String toString() { 47 return String.format("[%s] urgency=%-1d %-12s %-10s %s", 48 ticketId, urgency, category, customerId, summary); 49 } 50}
1// File: EscalationEngine.java 2 3import java.util.ArrayList; 4import java.util.List; 5import java.util.PriorityQueue; 6 7public class EscalationEngine { 8 9 // Min-heap ordered by urgency then age — critical+old tickets polled first 10 private final PriorityQueue<SupportTicket> queue = new PriorityQueue<>(); 11 private static final long ESCALATION_THRESHOLD_MS = 100; // low for demo 12 13 public void submit(SupportTicket ticket) { 14 queue.offer(ticket); 15 System.out.println(" SUBMITTED : " + ticket); 16 } 17 18 // Escalate any ticket older than the threshold 19 // NOTE: PriorityQueue does not re-heapify on field mutation. 20 // Correct approach: drain, escalate, rebuild. 21 public void runEscalationSweep() { 22 System.out.println("\n--- Escalation sweep ---"); 23 List<SupportTicket> all = new ArrayList<>(queue.size()); 24 while (!queue.isEmpty()) { 25 all.add(queue.poll()); 26 } 27 for (SupportTicket ticket : all) { 28 long ageMs = System.currentTimeMillis() - ticket.getCreatedAtMs(); 29 if (ageMs > ESCALATION_THRESHOLD_MS && ticket.getUrgency() > 1) { 30 ticket.escalate(); 31 } 32 } 33 queue.addAll(all); // rebuilds heap via Floyd's O(n) heapify 34 } 35 36 public SupportTicket dispatchNext() { 37 SupportTicket ticket = queue.poll(); 38 if (ticket != null) { 39 System.out.println(" DISPATCHED : " + ticket); 40 } else { 41 System.out.println(" No tickets pending."); 42 } 43 return ticket; 44 } 45 46 public void printQueueState() { 47 System.out.println("=".repeat(68)); 48 System.out.printf(" QUEUE STATE (%d tickets)%n", queue.size()); 49 System.out.println("=".repeat(68)); 50 // Cannot iterate in priority order — must drain a copy 51 PriorityQueue<SupportTicket> copy = new PriorityQueue<>(queue); 52 int rank = 1; 53 while (!copy.isEmpty()) { 54 System.out.printf(" %2d. %s%n", rank++, copy.poll()); 55 } 56 System.out.println("=".repeat(68)); 57 } 58 59 public static void main(String[] args) throws InterruptedException { 60 61 EscalationEngine engine = new EscalationEngine(); 62 63 System.out.println("--- Submitting tickets ---"); 64 engine.submit(new SupportTicket("TKT-001","C-Priya", "BILLING", 3, "Wrong cashback amount")); 65 engine.submit(new SupportTicket("TKT-002","C-Rohan", "TECHNICAL", 2, "OTP not received")); 66 engine.submit(new SupportTicket("TKT-003","C-Ananya", "BILLING", 1, "Payment charged twice")); 67 engine.submit(new SupportTicket("TKT-004","C-Karan", "COSMETIC", 4, "Button colour is off")); 68 engine.submit(new SupportTicket("TKT-005","C-Divya", "TECHNICAL", 2, "App crash on login")); 69 70 System.out.println(); 71 engine.printQueueState(); 72 73 System.out.println("\n--- Dispatching 2 tickets ---"); 74 engine.dispatchNext(); // TKT-003 urgency=1 first 75 engine.dispatchNext(); // TKT-002 or TKT-005 urgency=2 76 77 Thread.sleep(150); // age all remaining tickets past threshold 78 engine.runEscalationSweep(); 79 80 System.out.println("\n--- Queue after escalation ---"); 81 engine.printQueueState(); 82 83 System.out.println("\n--- Dispatching remaining ---"); 84 while (!engine.queue.isEmpty()) { 85 engine.dispatchNext(); 86 } 87 } 88}
Output:
--- Submitting tickets ---
  SUBMITTED  : [TKT-001] urgency=3  BILLING       C-Priya     Wrong cashback amount
  SUBMITTED  : [TKT-002] urgency=2  TECHNICAL     C-Rohan     OTP not received
  SUBMITTED  : [TKT-003] urgency=1  BILLING       C-Ananya    Payment charged twice
  SUBMITTED  : [TKT-004] urgency=4  COSMETIC      C-Karan     Button colour is off
  SUBMITTED  : [TKT-005] urgency=2  TECHNICAL     C-Divya     App crash on login

====================================================================
  QUEUE STATE  (5 tickets)
====================================================================
   1. [TKT-003] urgency=1  BILLING       C-Ananya    Payment charged twice
   2. [TKT-002] urgency=2  TECHNICAL     C-Rohan     OTP not received
   3. [TKT-005] urgency=2  TECHNICAL     C-Divya     App crash on login
   4. [TKT-001] urgency=3  BILLING       C-Priya     Wrong cashback amount
   5. [TKT-004] urgency=4  COSMETIC      C-Karan     Button colour is off
====================================================================

--- Dispatching 2 tickets ---
  DISPATCHED : [TKT-003] urgency=1  BILLING       C-Ananya    Payment charged twice
  DISPATCHED : [TKT-002] urgency=2  TECHNICAL     C-Rohan     OTP not received

--- Escalation sweep ---
  ESCALATED: TKT-005 → urgency 1
  ESCALATED: TKT-001 → urgency 2
  ESCALATED: TKT-004 → urgency 3

--- Queue after escalation ---
====================================================================
  QUEUE STATE  (3 tickets)
====================================================================
   1. [TKT-005] urgency=1  TECHNICAL     C-Divya     App crash on login
   2. [TKT-001] urgency=2  BILLING       C-Priya     Wrong cashback amount
   3. [TKT-004] urgency=3  COSMETIC      C-Karan     Button colour is off
====================================================================

--- Dispatching remaining ---
  DISPATCHED : [TKT-005] urgency=1  TECHNICAL     C-Divya     App crash on login
  DISPATCHED : [TKT-001] urgency=2  BILLING       C-Priya     Wrong cashback amount
  DISPATCHED : [TKT-004] urgency=3  COSMETIC      C-Karan     Button colour is off

The runEscalationSweep() method demonstrates the correct pattern for modifying priority fields after insertion: drain the entire queue, update the fields, and rebuild with addAll(). In-place mutation of comparison fields without rebuilding leaves the heap in an inconsistent state — a production bug that is notoriously hard to diagnose.

Performance Considerations

OperationPriorityQueueArrayDequeTreeSet
offer(e)O(log n)O(1) amortisedO(log n)
poll()O(log n)O(1) amortisedO(log n)
peek()O(1)O(1)O(log n)
contains(e)O(n)O(n)O(log n)
remove(e)O(n)O(n)O(log n)
Build from CollectionO(n) Floyd'sO(n)O(n log n)
Iteration orderHeap array orderFIFO orderSorted order
Duplicate elementsAllowedAllowedRejected
Memory per element~4-8 bytes (array)~4-8 bytes (array)~56 bytes (tree node)

O(n) vs O(n log n) construction: Building a PriorityQueue from a Collection via the PriorityQueue(Collection) constructor uses Floyd's heapify and is O(n). Inserting n elements one by one with offer() is O(n log n). When initialising from an existing collection, always use the constructor.

contains() and remove() are O(n): Unlike TreeSet, PriorityQueue has no fast path for membership testing or value-based removal. It must scan the entire array linearly. For workloads that require frequent contains() or targeted remove() by value, combine PriorityQueue with a HashMap to maintain a fast lookup structure alongside the heap.

Thread safety: PriorityQueue is not thread-safe. For concurrent access, use PriorityBlockingQueue from java.util.concurrent — it has the same heap semantics but with a ReentrantLock for thread safety and blocking take() that waits when the queue is empty.

Best Practices

Always include a tiebreaker in Comparator or compareTo(). Two elements where compare(a, b) == 0 are both stored — PriorityQueue allows duplicates — but their relative processing order is undefined. For incident queues, task schedulers, or any use case where arrival order should determine tie resolution, add a secondary comparison field such as a unique ID or creation timestamp: Comparator.comparingInt(Task::priority).thenComparing(Task::id).

Never mutate a field used in priority comparison after insertion. PriorityQueue fixes the heap position at insertion time. Changing the priority field of an element that is already inside the queue corrupts the heap property — the element is in the wrong position and poll() returns incorrect results. The correct escalation pattern is to drain all elements, update the fields, and rebuild the heap with addAll().

For top-K problems, maintain a heap of exactly size K. A max-heap of size K gives you K smallest elements; a min-heap of size K gives you K largest. After each insertion, if heap.size() > k, call poll() to evict the out-of-range element. This keeps memory usage at O(k) rather than O(n) — critical for streaming datasets where n can be millions.

To iterate in priority order, drain a copy — never use for-each on the original. for (T item : priorityQueue) traverses the heap array, not the priority order. If you need to inspect all elements in priority order without destroying the queue: PriorityQueue<T> copy = new PriorityQueue<>(original); while (!copy.isEmpty()) { process(copy.poll()); }. The constructor copy triggers O(n) Floyd's heapify on the copy.

Common Mistakes

Mistake 1 — Expecting for-each to Iterate in Priority Order

1PriorityQueue<Integer> pq = new PriorityQueue<>(); 2pq.offer(50); pq.offer(10); pq.offer(30); pq.offer(20); pq.offer(40); 3 4// WRONG — for-each iterates heap array order, NOT priority order 5System.out.println("for-each: " + pq); // [10, 20, 30, 50, 40] — heap array, not sorted 6 7// CORRECT — poll() gives priority order (smallest first for default min-heap) 8System.out.print("poll() order: "); 9while (!pq.isEmpty()) { 10 System.out.print(pq.poll() + " "); // 10 20 30 40 50 — guaranteed ascending 11} 12System.out.println();
Output:
for-each: [10, 20, 30, 50, 40]
poll() order: 10 20 30 40 50

Mistake 2 — Mutating a Priority Field After Insertion

1// WRONG — changing priority after insertion corrupts heap position 2class Job { String id; int priority; } 3PriorityQueue<Job> queue = new PriorityQueue<>( 4 Comparator.comparingInt(j -> j.priority) 5); 6Job job = new Job(); job.id = "J1"; job.priority = 5; 7queue.offer(job); 8 9job.priority = 1; // heap is now inconsistent — job is in wrong bucket 10 11// queue.poll() may NOT return job first, even though it now has priority 1 12// The heap array position was fixed when priority was 5 13 14// CORRECT — drain, update, rebuild 15List<Job> all = new ArrayList<>(queue); 16queue.clear(); 17job.priority = 1; // mutate before re-adding 18queue.addAll(all); // Floyd's O(n) rebuild

Mistake 3 — Using PriorityQueue When FIFO Within Same Priority Is Required

1// WRONG — PriorityQueue does not guarantee arrival-order within equal priority 2PriorityQueue<String> messages = new PriorityQueue<>(); 3messages.offer("MSG-001"); 4messages.offer("MSG-002"); 5messages.offer("MSG-003"); 6 7// Expected FIFO: MSG-001, MSG-002, MSG-003 8// Actual: order among equal-priority elements is heap-implementation-defined 9 10// CORRECT — use ArrayDeque for strict FIFO 11java.util.Deque<String> fifoMessages = new java.util.ArrayDeque<>(); 12fifoMessages.offer("MSG-001"); 13fifoMessages.offer("MSG-002"); 14fifoMessages.offer("MSG-003"); 15// Guaranteed: MSG-001, MSG-002, MSG-003

Mistake 4 — Using offer() in a Loop When Constructor Can Build in O(n)

1List<Integer> sourceData = new ArrayList<>(List.of(88, 45, 92, 67, 78, 55, 95)); 2 3// WRONG — O(n log n): each offer() triggers sift-up 4PriorityQueue<Integer> slow = new PriorityQueue<>(); 5for (int value : sourceData) { 6 slow.offer(value); // n insertions, each O(log n) 7} 8 9// CORRECT — O(n): constructor triggers Floyd's heapify 10PriorityQueue<Integer> fast = new PriorityQueue<>(sourceData); // single O(n) pass

Interview Questions

Q1. What is PriorityQueue in Java and how does it differ from a regular Queue?

PriorityQueue<E> is a Queue implementation backed by a binary min-heap. Unlike ArrayDeque or LinkedList, which serve elements in FIFO (arrival) order, PriorityQueue always serves the element with the highest priority — by default, the smallest element via natural ordering. offer() and poll() are O(log n) because every insertion triggers a sift-up and every removal triggers a sift-down to restore the heap property. peek() is O(1) because the minimum is always at the root (index 0). Iteration via for-each does NOT traverse in priority order — it traverses the internal heap array.

Q2. How does the binary min-heap work internally in PriorityQueue?

The heap is stored in a flat Object[] array. For any element at index i, its parent is at (i-1)/2, its left child at 2*i+1, and its right child at 2*i+2. The heap property: every parent is smaller than or equal to its children. When offer(e) is called, e is appended at the end (next leaf) and sifted up — repeatedly swapped with its parent until it is no longer smaller than its parent, or it reaches the root. When poll() is called, the root is removed, the last element is placed at the root, and sifted down — repeatedly swapped with the smaller child until both children are larger. Both operations traverse O(log n) levels.

Q3. What is Floyd's heapify algorithm and why does it matter?

Floyd's heapify builds a valid heap from an arbitrary array in O(n) time by processing all non-leaf nodes from right to left, sifting each one down. The reason it is O(n) rather than O(n log n) is mathematical: most nodes in a complete binary tree are near the leaves where sift-down paths are short. When you construct a PriorityQueue from a Collection using the constructor new PriorityQueue<>(collection), Java uses Floyd's algorithm. When you insert elements one by one with offer(), each insertion is O(log n), giving O(n log n) total. For bulk initialisation, the constructor is always faster.

Q4. How do you implement a max-heap using PriorityQueue?

Pass Comparator.reverseOrder() to the constructor: new PriorityQueue<>(Comparator.reverseOrder()). This inverts the natural ordering so the largest element is the root. For custom classes, use a reversed comparator: Comparator.comparingInt(Task::getPriority).reversed(). The internal heap structure is unchanged — only the comparison direction is flipped, so all O(log n) sift-up and sift-down operations still apply correctly.

Q5. How would you find the K largest elements in a stream using PriorityQueue?

Maintain a min-heap of size K. For each incoming element, offer() it into the heap. If heap.size() > K, call poll() to remove the current minimum. After processing all elements, the heap contains exactly the K largest elements — the minimum in the heap is the Kth largest. This approach is O(n log K) time and O(K) space, which is optimal for streaming data where n can be very large. The inverse — K smallest using a max-heap — works symmetrically.

Q6. What happens when you modify a PriorityQueue element's comparison field after insertion?

The heap position is fixed at the time of insertion based on the element's priority value. Changing the priority field after insertion does not trigger any re-heapification — the heap is now inconsistent. The element stays in its old position, violating the heap property for all its ancestors and descendants. Subsequent poll() calls may return elements in incorrect priority order. The correct approach is to drain the entire queue, update the field, and rebuild using addAll(), which triggers Floyd's O(n) heapify.

FAQs

Does PriorityQueue allow null elements?

No. PriorityQueue throws NullPointerException on offer(null). Every insertion requires a compareTo() or Comparator comparison to find the correct heap position. Comparing any element with null throws NullPointerException. If a "no value" sentinel is needed, use Optional<E> as the element type or a dedicated non-null constant.

Can PriorityQueue contain duplicate elements?

Yes. Unlike TreeSet, which treats compare == 0 as "same element" and rejects the duplicate, PriorityQueue stores both. Two elements where compare(a, b) == 0 are both inserted and both available for poll(). Their relative dispatch order among equal-priority elements is heap-implementation-defined — no FIFO guarantee within the same priority level.

Is PriorityQueue the same as a sorted list?

No. The internal array is a heap, not a sorted array. Only the root (index 0) is guaranteed to be the minimum. The rest of the array satisfies the heap property (parent ≤ children) but is not in any particular sorted order. A sorted array would require O(n log n) to build and O(n) to insert while maintaining sorted order. The heap builds in O(n) (Floyd's) and inserts in O(log n), at the cost of not being sortable for free.

What is the difference between PriorityQueue and TreeSet?

Both maintain elements in an ordered fashion, but serve completely different purposes. PriorityQueue allows duplicates, is backed by a binary heap, and provides O(1) peek() and O(log n) poll() for the minimum. It has no fast contains() or remove(e) — both are O(n). TreeSet rejects duplicates, is backed by a Red-Black tree, and provides O(log n) for all operations including contains(), remove(), floor(), and ceiling(). Use PriorityQueue when you only need "next minimum" access and duplicates are expected. Use TreeSet when you need sorted unique elements with range navigation.

Why does PriorityQueue print elements out of order using toString()?

PriorityQueue.toString() delegates to AbstractCollection.toString(), which iterates using the collection's iterator. The iterator traverses the internal heap array in index order (0, 1, 2, ...), which is NOT sorted order. The heap array satisfies the heap property but is not a sorted array. Only the root is guaranteed to be the minimum. This confuses beginners who expect println(pq) to show sorted output.

How do you create a PriorityQueue for String elements in reverse alphabetical order?

Pass a comparator using Comparator.reverseOrder() or Collections.reverseOrder() to the constructor: new PriorityQueue<>(Comparator.reverseOrder()). This creates a max-heap where poll() returns the lexicographically largest string first. For a custom multi-field ordering, compose with thenComparing().

Summary

PriorityQueue<E> is Java's binary min-heap implementation of the Queue interface. Every offer() triggers a sift-up to restore the heap property; every poll() triggers a sift-down. Both are O(log n). peek() is O(1) because the minimum always lives at the root. Creating from a collection uses Floyd's O(n) heapify — always faster than sequential offers.

The three rules that prevent every common PriorityQueue bug: never expect for-each to iterate in priority order, never mutate a priority field after insertion without rebuilding the heap, and always include a tiebreaker in compareTo() or Comparator to ensure stable ordering within equal-priority elements.

The most important interview patterns: max-heap via Comparator.reverseOrder(), K smallest with a max-heap of size K, K largest with a min-heap of size K, and the correct escalation pattern of drain-update-rebuild. These cover the majority of PriorityQueue questions from service-based fresher rounds through product-company technical interviews.

What to Read Next