A Queue is a collection that orders elements for processing, with the most common ordering being first-in-first-out (FIFO). Think of an order processing pipeline: the order placed first is the order processed first. This lesson covers what Queue adds on top of Collection, the two parallel families of methods it defines (one that throws, one that returns a special value), why both families exist, and where the major sub-interfaces fit in the rest of this section.
Queue extends Collection, so every queue is also a collection. What it adds is the idea of a head and a tail. New elements are added at the tail; elements are taken out at the head. For a plain FIFO queue, that means the element that has been waiting the longest is the next one to come out.
That matches a lot of real workflows. A customer support system holds open tickets in arrival order so that whoever waited longest gets answered next. A print spooler holds documents in submission order. An order processing service holds new orders in a queue, and a worker pulls them off one at a time. Each of these is a queue: things go in at one end, things come out at the other end, and the order matters.
The interface itself is defined in java.util:
Six methods, organized as three pairs. Each pair does the same job, but the two members of a pair react differently when the queue can't satisfy the request. The next two sections walk through that.
Here is the shape of a basic FIFO queue:
Orders enter on the left (the tail) and leave on the right (the head). Order #1001 was placed first, so it sits closest to the head and will be the next one processed. Order #1004 was placed most recently, so it sits at the tail and will wait the longest.
A small program shows the FIFO behavior with the simplest concrete queue, LinkedList, which implements Queue:
Orders come out in the same order they went in. The first call to peek() looks at the head without removing it; each poll() removes and returns the head until the queue is empty.
The three operations a queue supports (insert, remove, inspect) each come in two variants. One throws an exception when the operation can't be done; the other returns a special value (false or null).
| Operation | Throws Exception | Returns Special Value |
|---|---|---|
| Insert | add(e) | offer(e) |
| Remove (head) | remove() | poll() |
| Inspect (head) | element() | peek() |
Read the table as three rows of two equivalents. add and offer both add an element. remove and poll both remove and return the head. element and peek both look at the head without removing it. The difference is purely about what happens when the queue can't fulfill the request.
The "can't fulfill" cases break into two situations. For removal and inspection, the queue might be empty, so there's no head. For insertion, the queue might be full (for queues with a fixed capacity), so there's no room.
The throwing family handles those cases by raising exceptions:
| Method | Failure case | Exception thrown |
|---|---|---|
add(e) | Queue is full | IllegalStateException |
remove() | Queue is empty | NoSuchElementException |
element() | Queue is empty | NoSuchElementException |
The special-value family handles those cases by returning a sentinel instead:
| Method | Failure case | Return value |
|---|---|---|
offer(e) | Queue is full | false |
poll() | Queue is empty | null |
peek() | Queue is empty | null |
A short program shows both families side by side. Consider a print job queue.
element() and remove() worked fine because the queue had one job. After the removal, the queue was empty, and peek() and poll() returned null instead of throwing. The final offer succeeded because LinkedList has no capacity limit, so the boolean is true.
Compare what happens if you call the throwing variants on an empty queue:
Both throw NoSuchElementException because there's no head to remove or look at. The peer methods poll and peek would have returned null quietly for the same situation.
If the two families do the same job, why does the API ship both? The answer is that the throwing family was inherited from an older design, and the special-value family was added for queues that have a capacity limit, where "can't fit" is a normal outcome rather than a bug.
A capacity-restricted queue is one with a maximum number of elements it can hold. ArrayBlockingQueue (in the concurrent collections) is the canonical example: you create it with a fixed capacity, and once it's full, any further inserts have to deal with the "no room" case somehow. For a queue like that, callers regularly need to ask "did this fit?" without treating a "no" as a programming error. Returning false from offer is the right shape for that question. Throwing IllegalStateException from add would force every caller to write a try-catch around an ordinary, expected outcome.
The same logic applies to removal and inspection. For a queue that's empty most of the time and only sometimes has work, polling returns null cleanly. The caller checks the return value and moves on. The throwing variant would force the same calling code to wrap every removal in a try-catch.
A rough guide for which family to use:
| Situation | Prefer |
|---|---|
| Empty queue is expected (the worker loop pattern) | poll, peek |
| Empty queue would be a bug worth surfacing | remove, element |
| Bounded queue where "full" is a normal outcome | offer |
| Unbounded queue or a full queue would be a bug | add |
Most everyday code uses the special-value family (offer, poll, peek) because it composes well with the typical "drain the queue until it's empty" loop:
The loop condition does the dual job of removing the head and checking whether the queue is empty. The null return is what stops the loop. Rewriting the same loop with remove() would need a separate isEmpty() check or a try-catch around remove() to handle the empty case, which is more code for the same behavior.
poll and peek on LinkedList are O(1) because the head pointer is right there. Don't index into the queue with get(i) to do the same job; LinkedList.get walks the chain from the head and is O(n).
A note on null elements. Because null is the sentinel that poll and peek use to signal "empty", most Queue implementations refuse to hold null as a real element. LinkedList is the lenient one and will let you add null, but the moment you do, you can't tell from a poll whether you got a real null element or an empty queue. The general rule is: don't put null in a queue. Most implementations (ArrayDeque, PriorityQueue, the blocking queues) will throw NullPointerException if you try.
Queue is a base interface, and the concrete behavior you actually use comes from its sub-interfaces and implementing classes. Three of them are worth naming now.
`PriorityQueue` implements Queue but does not use FIFO order. Instead, it orders elements by priority. The head is always the smallest element according to the queue's comparator (or the natural ordering of the elements). A support system that processes high-severity tickets ahead of low-severity ones is a natural fit. The method names are the same (offer, poll, peek), so a piece of code written against the Queue interface can swap in a PriorityQueue without changes, even though the ordering rule is completely different.
`Deque` ("double-ended queue") extends Queue and adds operations for both ends. You can insert and remove from the head or the tail, which means a Deque can act like a plain FIFO queue or like a LIFO stack, depending on which methods you use. ArrayDeque is the typical implementation and is recommended over the legacy Stack class when you need stack behavior.
`BlockingQueue` lives in java.util.concurrent. It extends Queue and adds methods that wait for the queue to become non-empty (for consumers) or non-full (for producers) instead of returning immediately. It's the standard data structure for producer-consumer patterns across threads. You'd use it in concurrent code; it sits in this same family of interfaces.
The concrete classes used most often:
| Class | Sub-interface | Ordering | Capacity |
|---|---|---|---|
LinkedList | Queue, Deque | FIFO | Unbounded |
ArrayDeque | Deque | FIFO or LIFO | Unbounded |
PriorityQueue | Queue | By priority | Unbounded |
ArrayBlockingQueue | BlockingQueue | FIFO | Bounded |
For a plain FIFO queue today, prefer ArrayDeque because it's faster and uses less memory than LinkedList. LinkedList is convenient when you happen to have one around already, and the examples above use it because it's the simplest concrete Queue that's been around since Java 1.2.
A useful habit when you're holding a queue is to declare the variable using the Queue interface rather than the concrete class. The benefit is that the method bodies that work with the queue only use the six interface methods, so swapping implementations later is a single-line change.
The process method takes a Queue<String>, not a LinkedList<String>. If someone later wants to use an ArrayDeque (for speed) or a PriorityQueue (to order by priority), the method body doesn't need to change. The only line that changes is the variable declaration in main.
A small contract reminder: Queue inherits everything from Collection, so iteration is allowed, but the iteration order is not guaranteed to be the same as the queue's head-to-tail order for every implementation. PriorityQueue, for instance, iterates in no particular order. If you need to walk a queue in head-to-tail order, drain it with poll() in a loop.
10 quizzes