Back to Blog
Java

Java Queue: Interface, Implementations, and Usage

java queue: Understand the Java Queue interface, its core implementations, performance tradeoffs, and concurrency behavior for practical developer use.

QueueJava CollectionsConcurrencyData Structures
Illustration of a Java Queue with elements entering and leaving in FIFO order, representing the java.util.Queue interface and its implementations.

java queue requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

The java.util.Queue interface is a core part of the Java Collections Framework. It defines a collection designed to hold elements prior to processing, with operations that follow a FIFO (first-in, first-out) order in most implementations. The interface itself does not mandate FIFO, but the common contract and most implementations follow it. As a developer, you need to know not only the interface methods but also how each implementation behaves in terms of ordering, null handling, thread safety, and performance.

The Queue Interface and Its Contract

The Queue interface extends Collection and adds six main methods that fall into two categories: those that throw exceptions when they fail, and those that return a special value (like null or false). The table below summarizes the operations:

OperationThrows ExceptionReturns Special Value
Insertadd(e)offer(e)
Removeremove()poll()
Examineelement()peek()

add(e) throws IllegalStateException if the queue cannot accept the element (e.g., capacity is full). offer(e) returns false instead. Similarly, remove() throws NoSuchElementException if the queue is empty, while poll() returns null. element() throws if empty, peek() returns null. This distinction is crucial when you write code that must handle queue-full or queue-empty conditions gracefully.

The Queue interface also inherits Collection methods like size(), isEmpty(), and contains(), but the six methods above are the ones you will use most often.

Core Implementations and Their Characteristics

Java provides several implementations of Queue, each with different ordering, null, and thread-safety characteristics. The most commonly used are:

  • LinkedList – A doubly-linked list that implements both List and Deque. It allows null elements and is not thread-safe.
  • ArrayDeque – A resizable-array implementation of Deque. It does not allow null elements and is not thread-safe.
  • PriorityQueue – An unbounded priority queue based on a heap. It orders elements according to their natural ordering or a provided Comparator. It does not allow null.
  • BlockingQueue implementations (e.g., ArrayBlockingQueue, LinkedBlockingQueue) – Designed for concurrent producer-consumer scenarios. They support blocking operations like put() and take().

The following table compares these implementations across key dimensions:

ImplementationOrderingNulls AllowedThread-SafeTypical Use Case
LinkedListFIFOYesNoGeneral-purpose queue, also used as a list
ArrayDequeFIFO (when used as queue)NoNoHigh-performance queue/deque, no nulls needed
PriorityQueuePriority orderNoNoProcessing elements by priority
ArrayBlockingQueueFIFONoYesBounded producer-consumer
LinkedBlockingQueueFIFONoYesUnbounded or bounded producer-consumer

Note that ArrayDeque and PriorityQueue do not allow null elements. If you need to store null, LinkedList is the only standard non-blocking queue that permits it. For blocking queues, ArrayBlockingQueue and LinkedBlockingQueue also reject null.

Choosing Between LinkedList and ArrayDeque

When you need a simple FIFO queue and do not require null elements, ArrayDeque is usually a better choice than LinkedList. The reason is performance and memory footprint. ArrayDeque uses a resizable array internally, which provides better cache locality and lower per-element overhead compared to the doubly-linked list nodes used by LinkedList. Each node in a LinkedList stores two references (next and previous) plus the element itself, adding significant memory overhead. ArrayDeque also avoids the overhead of node allocation for each element; it simply writes into an array.

For example, consider a typical queue usage:

Queue<String> queue = new ArrayDeque<>(); queue.offer("first"); queue.offer("second"); String head = queue.poll(); // "first"

If you need to store null values, or you also need List operations (like random access by index), then LinkedList is appropriate. But if you only need queue semantics, ArrayDeque is the recommended implementation in most cases.

PriorityQueue: When Ordering Matters

PriorityQueue is not FIFO; it orders elements according to their natural ordering or a custom Comparator. The head of the queue is the least element according to the specified ordering. This makes it useful for task scheduling, Dijkstra's algorithm, or any scenario where you need to process items by priority.

PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.offer(5); pq.offer(1); pq.offer(3); int first = pq.poll(); // 1

The time complexity for offer and poll is O(log n) because of the underlying heap. This is slower than the O(1) amortized for ArrayDeque, but it is the cost of maintaining order. PriorityQueue is not thread-safe; if multiple threads access it concurrently, you must synchronize externally or use a PriorityBlockingQueue.

When using PriorityQueue, be aware that the iterator does not guarantee any particular order. If you need to traverse elements in priority order, you must repeatedly call poll().

BlockingQueue for Producer-Consumer Scenarios

For concurrent applications, the BlockingQueue interface extends Queue and adds blocking operations. The most important are put(e) and take(). put(e) inserts the element, waiting if necessary for space to become available. take() retrieves and removes the head, waiting if necessary for an element to appear. These methods are ideal for implementing producer-consumer patterns where producers and consumers run in separate threads.

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

ArrayBlockingQueue is a bounded, FIFO queue backed by an array. LinkedBlockingQueue is optionally bounded and uses linked nodes. Both are thread-safe and do not allow null elements. When using a bounded queue, you must handle the case where put blocks indefinitely; you can use offer(e, timeout, unit) to wait with a timeout instead.

For unbounded queues, put never blocks, but the queue can grow without limit, which may lead to memory exhaustion. Always consider whether you need a bound to protect against excessive memory usage.

Performance and Memory Considerations

The performance of queue operations varies significantly by implementation. ArrayDeque offers amortized O(1) time for offer, poll, peek, and size. LinkedList also offers O(1) for offer and poll, but the constant factors are higher due to node allocation and pointer chasing. PriorityQueue offers O(log n) for offer and poll. Blocking queues add synchronization overhead, but ArrayBlockingQueue uses a single lock and two conditions, making it efficient for moderate concurrency.

Memory usage is another factor. ArrayDeque uses a contiguous array that may need to be resized when it grows. The resizing operation copies elements, which is O(n) but amortized O(1) per insertion. LinkedList allocates a new node for each element, which has higher fixed overhead. PriorityQueue also uses an array, but it may be larger than the number of elements to maintain heap structure.

For most non-concurrent queue needs, ArrayDeque is the best default. If you need priority ordering, use PriorityQueue. If you need thread safety, choose an appropriate BlockingQueue implementation.

Common Pitfalls and Edge Cases

One common mistake is using add() on a bounded queue without checking the return value. If the queue is full, add() throws IllegalStateException, which may crash your application. Use offer() and check its boolean result when you cannot guarantee capacity.

Another pitfall is calling remove() or element() on an empty queue. These methods throw NoSuchElementException. In many cases, poll() and peek() are safer because they return null. However, if null is a valid element in your queue (only possible with LinkedList), you cannot distinguish between an empty queue and a null element. In that case, use isEmpty() before accessing the head.

When using PriorityQueue, remember that the natural ordering must be consistent with equals. If you use a custom comparator, ensure it defines a total order to avoid unpredictable behavior. Also, PriorityQueue does not allow null elements; attempting to add null will throw NullPointerException.

Finally, none of the non-blocking queue implementations are thread-safe. If multiple threads access the same queue instance, you must synchronize externally or use a BlockingQueue implementation. Even LinkedList and ArrayDeque will fail unpredictably if accessed concurrently without synchronization.

java queue: Practical Usage and Code Examples | RYUSLOG DEV