Queue and Deque in Java: Interfaces and Core Methods (A Beginner’s Guide)
-
Last Updated: August 11, 2026
-
By: javahandson
-
Series
Learn Queue and Deque in Java the easy way. Understand FIFO, core methods like offer, poll, and peek, two-ended Deque access, and when to use ArrayDeque.
The Queue and Deque in Java are two collection types built around one idea: the order in which items get served. You already know a List keeps items in a row. You also know a Set drops duplicates. Now comes a fresh shape of collection.
The Queue and Deque in Java care about one thing above all: order of service. They decide who gets handled first, and who waits.
Think of a line at a ticket counter. The person who came first gets served first. Nobody jumps ahead. That simple rule sits at the heart of a queue.
A Deque takes that idea and stretches it. With a Deque, you can add or remove from both ends. So it works as a queue, a stack, or both at once.
In this guide, we start with the Queue interface. After that, we open up the Deque interface and its handy two-ended methods.
Here is what we will cover:
No prior queue knowledge is needed. If you can use an ArrayList, you are ready for this.

A Queue is a collection built around waiting in line. Items enter at one end and leave from the other. The first item in is the first item out.
We call this rule FIFO, short for First In, First Out. It matches how a real queue behaves every single day.
Picture a queue at a coffee shop. You join at the back. You wait your turn. When you reach the front, you get served and leave.
A Java Queue works the same way. New items join the tail. Served items leave from the head. Order stays fair and predictable.
So where does a queue help in real code? Quite a few places, actually:
In each case, order matters. You want the oldest item handled first. A queue gives you that for free.
Let us make the rule concrete with a tiny example. Three people join a line, one after another.
Queue<String> line = new LinkedList<>();
line.offer("Amy"); // joins the back
line.offer("Ben"); // joins behind Amy
line.offer("Cara"); // joins behind Ben
// served in the same order they arrived
System.out.println(line.poll()); // Amy
System.out.println(line.poll()); // Ben
System.out.println(line.poll()); // CaraAmy came first, so Amy leaves first. Nobody skips the line. That predictable order is the whole promise of a queue.
Here is a point that trips up many beginners. You cannot write new Queue(). The Queue is an interface, so it only describes behaviour.
A concrete class does the actual work. Java hands you a few common choices:
Here is the usual way to declare one:
Queue<String> line = new LinkedList<>();
line.add("Amy");
line.add("Ben");
line.add("Cara");
System.out.println(line.poll()); // Amy leaves first
System.out.println(line); // [Ben, Cara]Notice the left side says Queue, not LinkedList. This is a good habit. You code to the interface, so swapping the class later stays painless.
A queue is great for order, but it is not a search tool. It does not let you jump to the middle or look up a value fast.
So skip a plain queue when you need any of these:
Pick the tool that matches your access pattern. A queue earns its place when you care about who goes first, not who sits where.
The Queue interface gives you a small, focused set of methods. The neat part is how they come in pairs. One pair throws on failure, the other returns a quiet signal.
Each core action has two versions. Both do the same job in normal cases. They differ only when something goes wrong, like an empty queue.
Here is the full pairing, side by side:
| Action | Throws on Failure | Returns Special Value |
|---|---|---|
| Insert | add(e) | offer(e) |
| Remove | remove() | poll() |
| Peek | element() | peek() |
Read that table row by row. Insert has add and offer. Remove has remove and poll. Peek has element and peek. Learn the pairs, and the whole interface clicks.
Both methods put an item at the tail of the queue. In everyday use, they behave the same.
The gap shows up in a bounded queue, one with a size limit. When such a queue is full, add() throws an exception. But offer() simply returns false.
Queue<Integer> q = new LinkedList<>(); q.add(10); // returns true q.offer(20); // returns true System.out.println(q); // [10, 20]
For an unbounded queue like a plain LinkedList, either one is fine. Pick offer() when you expect limits and want to avoid exceptions.
Both methods take the item off the head of the queue. They also return that item to you.
The difference lands on an empty queue. Calling remove() throws a NoSuchElementException. Calling poll() just returns null.
Queue<String> q = new LinkedList<>();
q.offer("first");
q.offer("second");
String head = q.poll(); // "first"
System.out.println(head);
System.out.println(q); // [second]| Interview Insight Q: Why prefer poll() over remove() in a loop? A: Because poll() returns null on an empty queue instead of throwing. That makes it clean to loop with while ((item = q.poll()) != null). You drain the queue without wrapping every call in a try-catch. |
Sometimes you want to see the head without removing it. That is what peeking does.
Both methods return the head item and leave it in place. On an empty queue, element() throws, while peek() returns null.
Queue<String> q = new LinkedList<>();
q.offer("job-1");
String next = q.peek(); // "job-1", still in the queue
System.out.println(next);
System.out.println(q); // [job-1]Peeking is handy when you want to check the next item before you commit to processing it. You look first, then decide.
Let us put the pairs together in one flow. We build a queue, peek at the head, then drain it one item at a time.
Queue<String> q = new LinkedList<>();
q.offer("a");
q.offer("b");
q.offer("c");
System.out.println(q.peek()); // "a" — head, not removed
while (!q.isEmpty()) {
System.out.println(q.poll()); // a, then b, then c
}
System.out.println(q.poll()); // null — queue is empty nowWatch the last line closely. Once the queue is empty, poll() hands back null. No exception, no crash. That quiet behaviour makes loops easy to write.
Speed matters when queues grow large. The good news is that the main operations are cheap for the common classes.
For both ArrayDeque and LinkedList, the core actions run in constant time. That means the cost stays the same whether you hold ten items or ten million.
An ArrayDeque may resize its backing array once in a while. That single resize is slower. But spread across many adds, the average stays constant. We call that amortized O(1).
| Interview Insight Q: Is ArrayDeque or LinkedList faster for a queue? A: ArrayDeque usually wins. It stores items in one contiguous array, so the CPU cache loves it. A LinkedList scatters nodes across memory and stores extra pointers per node. Both are O(1) for queue operations, but ArrayDeque tends to run faster and use less memory in practice. |
A Deque is a double-ended queue. The name is short for Double-Ended Queue, and folks pronounce it “deck”.
A plain queue lets you add at one end and remove from the other. A Deque removes that limit. You can add or remove at both the head and the tail.
Picture a deck of cards held in your hand. You can slide a card off the top. You can also slip one under the bottom. A Deque gives you that same freedom.
Because of this, a Deque can play two roles at once:
So one interface covers two classic data structures. That flexibility makes Deque a favourite in modern Java code.
The Deque interface builds right on top of Queue. In code terms, Deque extends Queue. So every Queue method still works on a Deque.
On top of those, Deque adds its own two-ended methods. You get First and Last versions of each action, which we will see next.
The most common class here is ArrayDeque. It is fast, array-backed, and great as both a queue and a stack:
Deque<String> dq = new ArrayDeque<>();
dq.offerFirst("B");
dq.offerFirst("A"); // A goes to the front
dq.offerLast("C"); // C goes to the back
System.out.println(dq); // [A, B, C]A Deque doubles the method count, but the pattern stays simple. Each action gets a First flavour and a Last flavour. And once again, each flavour has a throwing and a returning version.
Do not try to memorise this table. Just see the pattern. First works on the head, Last works on the tail.
| Action | Head — Throws | Head — Returns | Tail — Throws | Tail — Returns |
|---|---|---|---|---|
| Insert | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
| Remove | removeFirst() | pollFirst() | removeLast() | pollLast() |
| Peek | getFirst() | peekFirst() | getLast() | peekLast() |
Read it as a grid. Pick your end, then pick your failure style. That is the whole idea behind the Deque method names.
Let us watch a Deque in action from both sides. We add and remove at the head and the tail.
Deque<Integer> dq = new ArrayDeque<>(); dq.offerLast(1); // [1] dq.offerLast(2); // [1, 2] dq.offerFirst(0); // [0, 1, 2] System.out.println(dq.pollFirst()); // 0 System.out.println(dq.pollLast()); // 2 System.out.println(dq); // [1]
See how each call names its end? offerFirst touches the head. pollLast touches the tail. The names read almost like plain English.
Two-ended access is not just a party trick. Several real problems map onto it cleanly.
In each case, you need to touch both ends. A plain queue cannot do that. A Deque handles it in one clean interface.
You may have heard of the old Stack class in Java. It works, but the team now advises against it. A Deque is the modern replacement.
The legacy Stack extends Vector, so it carries synchronisation you rarely need. That extra locking slows things down for no gain.
The Java docs themselves point you to ArrayDeque for stack behaviour. It is faster and cleaner. So reach for a Deque instead.
For stack use, Deque gives you three friendly method names. They all act on the head of the deque.
Here is a small stack in action. Notice the LIFO order: last in, first out.
Deque<String> stack = new ArrayDeque<>();
stack.push("page-1");
stack.push("page-2");
stack.push("page-3");
System.out.println(stack.pop()); // page-3 (last in, first out)
System.out.println(stack.pop()); // page-2
System.out.println(stack.peek()); // page-1, still there| Interview Insight Q: Queue vs stack — how does a Deque switch between them? A: It is all about which end you touch. Use offer and poll for FIFO queue behaviour. Use push and pop for LIFO stack behaviour. Same Deque, two mental models, no new class needed. |
Stacks show up in real problems all the time. A classic one is checking whether brackets in a string are balanced.
The idea is simple. Push every opening bracket. On a closing bracket, pop and check the pair. If anything mismatches, the string is not balanced.
boolean isBalanced(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') {
stack.push(c); // opening bracket
} else if (c == ')' || c == ']' || c == '}') {
if (stack.isEmpty()) return false;
char open = stack.pop(); // must match
if (!matches(open, c)) return false;
}
}
return stack.isEmpty(); // nothing left over
}See how push and pop carry the whole logic? Each opening bracket waits on the stack. Each closing bracket checks the most recent one. That is LIFO doing real work.
Three words get thrown around a lot: queue, stack, and deque. They sound similar, so let us pin down the difference.
It comes down to which end you use.
So a Deque is the general case. A queue and a stack are just two ways of using it. Once that clicks, the whole family feels simple.
Here is the same idea in a grid. Notice how the Deque row covers both others.
| Type | Order | Add End | Remove End |
|---|---|---|---|
| Queue | FIFO | Tail | Head |
| Stack | LIFO | Head | Head |
| Deque | Both | Either | Either |
One small point about looping. When you iterate a Deque, it walks from head to tail by default.
For stack use, that can feel backwards. The head holds your most recent push, so iteration shows newest first. Keep that in mind when you print a Deque you are using as a stack.
You now know the interfaces. So which concrete class should you pick? It depends on what you need.
For most queue and stack needs, ArrayDeque is the go-to. It backs itself with a resizable array, so access stays fast.
A quick heads-up, though. An ArrayDeque does not allow null elements. Add a null, and it throws right away. That is by design.
A LinkedList implements both Deque and List. So you can treat it as a queue and index into it too.
That said, ArrayDeque usually wins on raw speed for pure queue work. Pick LinkedList only when you truly need List features as well.
Sometimes arrival order is not what you want. You want the smallest or highest-priority item first. That is where PriorityQueue shines.
It does not follow strict FIFO. Instead, it serves items by their natural order or a custom comparator. We cover it in depth in its own article.
Here is a quick comparison to keep things straight:
| Class | Best For | Watch Out For |
|---|---|---|
| ArrayDeque | Fast queue or stack | No null elements |
| LinkedList | Queue plus List access | Slower for pure queue use |
| PriorityQueue | Priority-based order | Not FIFO; no null |
The classes above are not thread-safe. If two threads touch the same queue at once, you can hit trouble. So what do you use then?
Java has a whole family built for concurrent work. You will find them in the java.util.concurrent package.
These shine in producer-consumer setups, where one thread adds jobs and another pulls them. We keep the deep dive for the concurrency series. For now, just know they exist.
A handful of traps catch beginners over and over. Let us name them so you can steer clear.
Many folks call add() when they wanted offer(). The mix-up bites only on a full bounded queue, where add() throws. Know which style you need before you type it.
If an empty or full case is normal in your flow, use the returning versions. So prefer offer, poll, and peek. They fail quietly with null or false.
As noted earlier, an ArrayDeque rejects null. New users try it and get a NullPointerException. If you must store nulls, a LinkedList allows them.
A plain Queue does not sort anything. It keeps arrival order, plain and simple. If you expect sorted output, you want a PriorityQueue instead.
Old tutorials still show the Stack class. Skip it in new code. An ArrayDeque used with push and pop does the same job faster.
These two look similar, but they behave differently. peek() only looks at the head. poll() looks and removes.
Beginners sometimes call peek() in a loop and wonder why it never ends. The head never leaves, so the loop spins forever. Use poll() when you mean to drain the queue.
Let us tie it together with a small, real task. Say you are building a simple task scheduler.
Jobs arrive over time, and you handle them in order. New jobs join the tail. You pull the next one from the head.
Queue<String> jobs = new ArrayDeque<>();
jobs.offer("send-email");
jobs.offer("build-report");
jobs.offer("backup-db");
while (!jobs.isEmpty()) {
String job = jobs.poll();
System.out.println("Running: " + job);
}
// Runs in arrival order: send-email, build-report, backup-dbEach poll() grabs the oldest job. The loop drains the queue in fair FIFO order. Clean and easy to reason about.
Now a job needs to jump the line. With a Deque, you push it to the front instead of the back.
Deque<String> jobs = new ArrayDeque<>();
jobs.offerLast("build-report");
jobs.offerLast("backup-db");
jobs.offerFirst("urgent-patch"); // jumps to the head
System.out.println(jobs.pollFirst()); // urgent-patchThe urgent job lands at the head. Your next poll picks it up first. This is exactly why the two-ended Deque is so useful.
Sometimes you want to peek before you commit. Say you only run a job if it is not a duplicate of the last one.
Deque<String> jobs = new ArrayDeque<>();
jobs.offer("build-report");
jobs.offer("build-report"); // a repeat slipped in
String last = null;
while (!jobs.isEmpty()) {
String next = jobs.peek(); // look, do not remove yet
if (next.equals(last)) {
jobs.poll(); // skip the duplicate
continue;
}
last = jobs.poll();
System.out.println("Running: " + last);
}The peek() lets you look ahead safely. You decide, then you poll. This look-then-act pattern comes up often in real queue code.
These come up often in Java interviews. Short, honest answers work best. The full question set, formatted for the blog, is provided separately.
We have gathered the common ones into a dedicated FAQ block. Look for it below the article on the site.
A: A Queue lets you add at one end and remove from the other, so it follows FIFO order. A Deque is a double-ended queue, so you can add or remove at both the head and the tail. That means a Deque can act as a queue or a stack, while a plain Queue cannot.
A: FIFO stands for First In, First Out. The item added first is the item removed first, just like people waiting in a line. New items join the tail, and served items leave from the head.
A: Each action has a throwing version and a returning version. The throwing version (add, remove, element) raises an exception on failure. The returning version (offer, poll, peek) hands back a quiet signal like false or null. Use the returning versions when an empty or full queue is a normal case.
A: ArrayDeque is usually the better choice. It stores items in one contiguous array, so it is cache-friendly and uses less memory. Pick LinkedList only when you also need List features such as index access. Both give O(1) queue operations.
A: The legacy Stack extends Vector, so it carries synchronisation you rarely need, and that slows it down. The Java docs recommend ArrayDeque for stack behaviour. Use push, pop, and peek on a Deque for a faster, cleaner LIFO stack.
A: No. ArrayDeque rejects null and throws a NullPointerException if you try to add one. This is by design, because null is used as a “queue is empty” signal by poll and peek. If you must store nulls, use a LinkedList instead.
A: Use push to add to the head, pop to remove from the head, and peek to look at the head. All three act on the same end, which gives you LIFO order. For example, push(“a”), push(“b”), then pop() returns “b” first.
A: No. A plain Queue keeps arrival order and does not sort anything. If you need items served by priority or sorted order, use a PriorityQueue, which orders items by their natural order or a custom comparator.
A: For ArrayDeque and LinkedList, offer, poll, and peek all run in O(1) time. ArrayDeque may resize its backing array occasionally, which is slower, but averaged over many adds the cost stays constant, known as amortized O(1).
A: LinkedList implements both Queue and Deque. ArrayDeque implements Deque and is the common default for queues and stacks. PriorityQueue implements Queue but orders items by priority rather than arrival, and it does not implement Deque.
Let us wrap up what we covered. A Queue models a waiting line, and it follows the FIFO rule.
Its core methods come in pairs. So add and offer insert, remove and poll take off the head, and element and peek look without removing.
A Deque widens the idea to both ends. You add or remove at the head or tail, which lets one interface act as both a queue and a stack.
For most work, reach for ArrayDeque. Pick LinkedList when you also need List access. And choose PriorityQueue when priority beats arrival order.
One last tip to carry with you. When an empty or full case is normal, lean on offer, poll, and peek. They fail quietly instead of throwing, which keeps your loops clean.
Master these two interfaces, and a big chunk of everyday Java gets easier. Scheduling, buffering, undo stacks, and graph traversal all lean on queues and deques under the hood.