Back to Blog
Java

Java PriorityQueue Usage: Ordering, Performance, and Pitfalls

java priorityqueue usage: Learn how to use Java PriorityQueue for ordered processing, custom comparators, and understand its performance tradeoffs and common mistakes.

PriorityQueueJava CollectionsHeapComparatorQueue
A visual metaphor for a Java PriorityQueue showing a heap structure with the smallest element at the top, representing ordered processing.

When you need to process items in a specific order without sorting the entire collection on each insertion, java.util.PriorityQueue is a direct answer. It implements a min-heap by default, meaning the smallest element (according to natural ordering) is always at the head. This article covers the practical usage of java priorityqueue usage: constructing queues, defining custom ordering, understanding runtime costs, and avoiding the pitfalls that commonly trip up developers.

A Minimal PriorityQueue Example

A PriorityQueue behaves like a queue, but poll() and peek() return the highest-priority element rather than the oldest one. The simplest usage relies on the natural ordering of the elements, which requires them to implement Comparable.

Queue<Integer> numbers = new PriorityQueue<>(); numbers.offer(5); numbers.offer(1); numbers.offer(3); System.out.println(numbers.peek()); // 1 System.out.println(numbers.poll()); // 1 System.out.println(numbers.poll()); // 3

The offer() method inserts an element, and poll() removes and returns the head. In this example, the queue returns integers in ascending order because Integer implements Comparable. The internal heap structure guarantees that peek() and poll() operate in O(log n) time, while offer() also runs in O(log n).

How PriorityQueue Orders Elements

The default ordering is the natural ordering defined by Comparable. For custom classes, you must either implement Comparable or supply a Comparator when constructing the queue. The head is the smallest element according to that ordering, so a min-heap is the default behavior. If you need a max-heap, you can reverse the comparator or use Collections.reverseOrder().

Queue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder()); maxHeap.offer(5); maxHeap.offer(1); maxHeap.offer(3); System.out.println(maxHeap.poll()); // 5

The ordering is not stable for equal elements. If two elements compare as equal, the queue does not guarantee which one comes out first. This is a consequence of the heap implementation, and you should not rely on insertion order for equal elements.

Using a Custom Comparator

When the natural ordering does not match your priority rules, pass a Comparator to the constructor. This is common when working with domain objects where the priority depends on a specific field or a computed value.

record Task(String name, int priority) {} Queue<Task> taskQueue = new PriorityQueue<>( Comparator.comparingInt(Task::priority) ); taskQueue.offer(new Task("write docs", 3)); taskQueue.offer(new Task("fix bug", 1)); taskQueue.offer(new Task("refactor", 2)); System.out.println(taskQueue.poll().name()); // fix bug

The comparator can be as complex as needed, but it must be consistent with equals(). If the comparator returns zero for two different objects, the queue will treat them as equal and may not preserve any order between them. Also, the comparator should not change over time while the queue is in use, because the heap invariant depends on a stable comparison.

Time Complexity and Memory Behavior

PriorityQueue is backed by an object array that grows automatically. The amortized cost of offer() is O(log n), but occasionally resizing the array adds a linear cost. poll() and peek() are O(log n) and O(1) respectively. contains() is O(n) because the heap does not maintain a search index; if you need frequent membership checks, consider a separate HashSet or a different data structure.

Memory usage is proportional to the number of elements. The backing array is not shrunk when elements are removed, so a queue that has seen many elements may retain a large array even after poll() empties it. This is usually acceptable, but if you are processing millions of items and want to free memory, you can create a new PriorityQueue when the queue becomes small.

PriorityQueue Is Not Thread-Safe

If multiple threads access the same PriorityQueue instance, you must synchronize externally. The class does not provide thread safety. For concurrent use, PriorityBlockingQueue is the standard alternative. It wraps the same heap logic with locks and provides blocking take() and put() methods.

BlockingQueue<Integer> concurrentQueue = new PriorityBlockingQueue<>();

When using PriorityBlockingQueue, iteration is still not thread-safe, and the iterator is fail-fast. If you need to iterate while other threads modify the queue, you must copy the elements into an array or use a snapshot.

Common Pitfalls: Nulls, Iteration, and Mutable Keys

Three mistakes appear frequently in real code. First, PriorityQueue does not allow null elements. Attempting to offer(null) throws NullPointerException because the comparator needs to compare the element with others. Second, iteration order is not sorted. The iterator of a PriorityQueue does not traverse the heap in priority order; it returns elements in an unspecified order. If you need to process elements in sorted order, you must repeatedly call poll() until the queue is empty.

Queue<Integer> queue = new PriorityQueue<>(List.of(3, 1, 2)); for (int value : queue) { // Order is not guaranteed; could be 3, 1, 2 or any permutation }

Third, mutable keys break the heap invariant. If an element's priority changes after it has been inserted, the queue will not reorder itself. The heap only maintains ordering based on the values at insertion time. If you need to update priorities, remove the element, change it, and re-insert it, or use a data structure designed for that purpose, such as a TreeSet with custom equality.

Choosing Between PriorityQueue and Other Collections

PriorityQueue is the right choice when you need efficient access to the smallest (or largest) element and you do not need to search by key. For a fully sorted collection that supports efficient lookup and removal of arbitrary elements, TreeSet provides O(log n) operations but requires unique elements and does not allow duplicates. PriorityQueue allows duplicates and is generally faster for insertion and removal of the head because it does not maintain a balanced tree.

If you need to iterate in sorted order without consuming the queue, copy the elements to an array and sort it. That gives you O(n log n) cost, which is acceptable for one-time processing. For a continuously changing set where you frequently need the smallest element, PriorityQueue is the standard solution in Java.

java priorityqueue usage: Practical Usage and Code Examples | RYUSLOG DEV