Back to Blog
Java

Java Queue Interface: Usage and Implementations

java queue interface: Learn how the Java Queue interface works, its core methods, common implementations, and how to choose the right one for your use case.

QueueJava CollectionsData StructuresConcurrencyPriorityQueue
Illustration of a Java Queue interface with elements entering and leaving in order, representing FIFO behavior.

The java queue interface defines a collection designed to hold elements prior to processing. Unlike a List, which allows random access, a Queue typically orders elements in a FIFO (first-in, first-out) manner, though the exact ordering depends on the implementation. The interface is part of java.util and provides methods for insertion, extraction, and inspection, each with two variants: one that throws an exception and one that returns a special value.

The Queue Interface and Its Contract

The Queue interface extends Collection and adds methods specific to queue behavior. The contract is simple: elements are added at the tail and removed from the head, but implementations like PriorityQueue change that ordering based on a comparator. The interface does not mandate FIFO; it only defines the operations. This flexibility is why you see different implementations with different ordering semantics.

Here is the core method set:

OperationThrows exceptionReturns special value
Insertadd(e)offer(e)
Removeremove()poll()
Examineelement()peek()

The special value is null for offer, poll, and peek when the operation cannot be performed. For add, remove, and element, an exception is thrown: IllegalStateException if the queue is full (for bounded queues), NoSuchElementException if the queue is empty. Understanding these two families is critical because mixing them up can lead to unexpected exceptions in production code.

Core Methods: Offer, Poll, and Peek

In most real-world code, you will use offer, poll, and peek because they do not throw exceptions. offer inserts an element if possible and returns true; otherwise it returns false. poll removes and returns the head, or returns null if empty. peek returns the head without removing it, or null if empty.

Queue<String> queue = new LinkedList<>(); queue.offer("first"); queue.offer("second"); String head = queue.peek(); // "first" String removed = queue.poll(); // "first" boolean empty = queue.isEmpty(); // false

Using add and remove is appropriate when you know the queue is not full or empty, and you want an exception to signal a programming error. For example, in a fixed-size ArrayBlockingQueue, add will throw if the queue is full, while offer will return false. The choice depends on how you want to handle failure.

Implementations: LinkedList vs ArrayDeque

LinkedList implements both List and Deque, making it a double-ended queue. It allows null elements, which can be problematic because poll and peek return null to indicate emptiness. If you store null in a LinkedList queue, you cannot distinguish between an empty queue and a queue with a null head. ArrayDeque, on the other hand, does not allow null elements and is generally more memory-efficient and faster for queue operations because it uses a resizable array.

Queue<String> linkedQueue = new LinkedList<>(); Queue<String> arrayQueue = new ArrayDeque<>();

For a simple FIFO queue, ArrayDeque is usually the better choice. It has lower overhead per element and does not suffer from the node allocation that LinkedList requires. However, LinkedList offers List operations like get(index) if you need that, but mixing queue and list semantics can be confusing.

PriorityQueue and Ordering

PriorityQueue is an implementation that orders elements according to their natural ordering or a custom Comparator. The head is always the smallest element according to the comparator. This is not FIFO; it is a priority-based ordering. PriorityQueue does not allow null and is not thread-safe.

Queue<Integer> priorityQueue = new PriorityQueue<>(); priorityQueue.offer(5); priorityQueue.offer(1); priorityQueue.offer(3); int head = priorityQueue.peek(); // 1

When you need to process elements in a specific order, such as the most urgent task first, PriorityQueue is the right tool. But be aware that iteration order is not guaranteed to be sorted; you must use poll repeatedly to get elements in order.

Blocking Queues for Concurrent Producers and Consumers

The BlockingQueue interface extends Queue and adds thread-safe operations that wait for space or elements to become available. Implementations like ArrayBlockingQueue, LinkedBlockingQueue, and PriorityBlockingQueue are designed for producer-consumer patterns. They support put and take, which block indefinitely, as well as timed variants like offer(e, timeout, unit) and poll(timeout, unit).

BlockingQueue<String> queue = new ArrayBlockingQueue<>(10); // Producer queue.put("item"); // Consumer String item = queue.take();

Using a blocking queue simplifies coordination between threads because the queue handles waiting and notification. The bounded version (ArrayBlockingQueue) prevents memory exhaustion by limiting capacity, while LinkedBlockingQueue can be unbounded but may grow without limit.

Choosing the Right Queue Implementation

Selecting a queue implementation depends on ordering requirements, thread safety, and capacity constraints. For a single-threaded FIFO queue, ArrayDeque is a solid default. If you need priority ordering, use PriorityQueue. For concurrent access, choose a BlockingQueue implementation that matches your capacity and fairness needs. LinkedList is rarely the best choice for a queue unless you also need list operations or must allow null elements.

Use caseRecommended implementation
Single-threaded FIFOArrayDeque
Priority orderingPriorityQueue
Concurrent producer-consumerArrayBlockingQueue or LinkedBlockingQueue
Need null elementsLinkedList (with caution)

Do not use PriorityQueue in a multithreaded environment without external synchronization; use PriorityBlockingQueue instead. Also, remember that ArrayDeque and PriorityQueue do not allow null, which is a safety feature that prevents ambiguous poll results.

Common Pitfall: Null Handling and Ambiguity

A frequent mistake is using a queue that permits null and then relying on poll to detect emptiness. If your queue can contain null, the result of poll on an empty queue and on a queue with a null head is identical: both return null. This ambiguity can lead to subtle bugs. The clean solution is to use an implementation that rejects null, such as ArrayDeque or PriorityQueue, or to check isEmpty() before calling poll.

Queue<String> queue = new LinkedList<>(); queue.offer(null); String value = queue.poll(); // null, but queue is not empty

Another pitfall is assuming that PriorityQueue preserves insertion order for equal elements. It does not; it only guarantees the head is the smallest. If you need stable ordering among equal elements, you must supply a custom comparator that breaks ties.

Performance Considerations Without Benchmarks

The performance of queue operations depends on the underlying data structure. ArrayDeque uses a circular array, so offer and poll are amortized O(1) and avoid node allocation. LinkedList also offers O(1) operations but with higher constant factors due to node creation and pointer updates. PriorityQueue uses a binary heap, so offer and poll are O(log n). Blocking queues add synchronization overhead, and ArrayBlockingQueue uses a single lock for both put and take, while LinkedBlockingQueue uses two locks, which can reduce contention under high throughput. These are structural characteristics, not measured benchmarks.

When you need to process a large number of elements with strict ordering, the logarithmic cost of PriorityQueue is unavoidable. For simple FIFO, the constant-time operations of ArrayDeque are usually sufficient. Always consider whether the ordering requirement is worth the extra cost.