Skip to content
Ameen Abozaid
PublishedBEG-040-L06

Choosing: ArrayList vs LinkedList vs ArrayDeque

  • Lesson 6 of 7
  • 11 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

You now know how an array behaves, how a dynamic array grows, how a linked list rewires, and how stacks and queues restrict access. This short lesson turns all of that into a decision. When you need a list, a stack, or a queue in Java, which class do you reach for?

The method is simple: decide by the operation you do most. The costs you have already derived tell you the answer. Think of it as choosing a vehicle: a bicycle, a bus, or a motorbike is not "best" in general, only best for a given trip.

Why this matters: wrong choices rarely crash a program. They make it slow in ways that appear only with real data. Being able to justify a choice in one sentence is a mark of a careful developer, and reviewers ask for it.

This lesson builds on every lesson in this module and on "Choosing a collection: decision chart" in the Collections module (BEG-030).

Learning Objectives

By the end of this lesson you will be able to:

  • State the main costs of ArrayList, LinkedList, and ArrayDeque for the common operations.
  • Explain why each cost is what it is, using arrays and nodes.
  • Pick a structure for a list, a stack, and a queue, and justify the choice.
  • Explain why ArrayList is the default and ArrayDeque is the default for stacks and queues.
  • Say what you must do before claiming one structure is faster than another.

Written Explanation

Three classes, three shapes

ClassShape insideInterfaces you use it through
ArrayListA resizable array (Lessons 1 and 2)List
LinkedListDoubly linked nodes (Lesson 3)List and Deque
ArrayDequeA resizable array used from both ends, as in Lesson 5's wrapping ideaDeque and Queue

The JDK documentation describes LinkedList as a doubly linked list that implements both List and Deque. ArrayDeque is a Deque backed by a resizable array. Both documentation pages describe ArrayList and ArrayDeque in terms of arrays, which is why their costs follow what you learned in Lessons 1 and 2.

The decision table

OperationArrayListLinkedListArrayDeque
Get by index get(i)O(1)O(n)not offered
Add at the endO(1) amortizedO(1)O(1) amortized
Add at the frontO(n)O(1)O(1) amortized
Remove from the frontO(n)O(1)O(1)
Remove from the endO(1)O(1)O(1)
Insert or remove in the middleO(n)O(n) to find, O(1) to relinknot offered
Search by value (contains)O(n)O(n)O(n)

Reasons behind the cells:

  • ArrayList.get(i) is the array arithmetic from Lesson 1; the ArrayList documentation says get runs in constant time and add in amortized constant time.
  • ArrayList front operations shift every item, as in Lesson 1.
  • LinkedList.get(i) has to walk. The documentation says index operations traverse from the beginning or the end, whichever is closer, which halves the walk at best but is still O(n).
  • LinkedList middle insert is O(1) only once you are already at the node, for example through an iterator. Finding it costs O(n). If you must search first, you pay the search.
  • ArrayDeque has no index methods at all, which is deliberate: it is designed for the ends. Trying d.get(0) on a Deque does not compile, and javac reports cannot find symbol ... method get(int).

Why LinkedList disappoints in practice

On paper the linked list wins at the front and in the middle. In practice it is rarely the best choice:

  • Each item needs a node object plus references, so it uses more memory per item than an array.
  • To use the "O(1) in the middle" advantage you need to already hold the position, which most programs do not.
  • For stacks and queues, ArrayDeque covers the ends just as well. Its documentation says it is likely to be faster than LinkedList when used as a queue, and faster than Stack when used as a stack.
  • Arrays keep items next to each other in memory, which processors usually handle well. How much this matters depends on the machine and the program, so treat it as a reason to measure, not as a guarantee.

The module's rule of thumb is: ArrayList for lists, ArrayDeque for stacks and queues, LinkedList only when you have a specific reason you can explain.

Decide by the operation you do most

Ask three questions in order:

  1. Do I need to read by position? (get(i), set(i, x).) Yes: ArrayList.
  2. Do I only add and remove at the ends, newest-first (stack) or oldest-first (queue)? Yes: ArrayDeque.
  3. Do I need to remove or insert in the middle often, while already holding a position? Maybe LinkedList, but first check whether a different structure (a Set or Map from the Collections module) fits better.

If none of these decide it, use ArrayList. Its documentation describes the constant-time operations that cover most everyday code.

Measure before claiming

A claim like "X is faster than Y" is only worth something with a measurement for your data and your machine. Quick timings written by hand in a main method are easily wrong, because the JVM warms up and optimizes as it runs. For this course, rely on the Big O reasoning above to choose, and use a proper benchmarking tool such as JMH in later work if performance really matters. Never write "linked lists are faster for insertion" as a blanket statement: it is true for the relinking step only.

Code against the interface

Declare variables with the interface, and choose the class once:

java
List<String> names = new ArrayList<>();
Deque<Integer> undo = new ArrayDeque<>();
Queue<String> jobs = new ArrayDeque<>();

If a measurement later shows a different class would fit, you change one line. Writing ArrayList<String> names everywhere would spread the choice through the code.

Examples

Example 1: the same data in three structures (simple)

java
List<String> arr = new ArrayList<>(List.of("a", "b", "c", "d"));
List<String> linked = new LinkedList<>(List.of("a", "b", "c", "d"));
Deque<String> deque = new ArrayDeque<>(List.of("a", "b", "c", "d"));

System.out.println(arr.get(2) + linked.get(2));
arr.remove(0);
linked.remove(0);
deque.pollFirst();
deque.addLast("e");
System.out.println(arr + " " + linked + " " + deque);

Real output:

text
cc
[b, c, d] [b, c, d] [b, c, d, e]

The three structures behave the same (same results from get(2), same list after removing the front); they differ only in cost: arr.remove(0) shifted three items, linked.remove(0) relinked the head, deque.pollFirst() advanced inside its array. That is the key lesson: the choice is about performance and API, not about different answers.

Example 2: a task queue with urgent jobs (practical)

A service has a queue of background jobs. Normal jobs join at the back and workers take from the front. An urgent job jumps to the front. A Deque supports that; a plain Queue does not.

java
Deque<String> work = new ArrayDeque<>();
work.offerLast("email");
work.offerLast("report");
work.offerFirst("urgent-fix");
System.out.println(work.pollFirst() + ", " + work.pollLast() + ", " + work);

Real output:

text
urgent-fix, report, [email]

ArrayDeque is the right choice: every operation is at an end, nothing needs an index, and there are no nulls in jobs. If urgency came in several levels, a priority queue (a later topic) would fit better than inserting at the front.

Example 3: stack or queue from the same class (realistic)

java
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); stack.push(2); stack.push(3);
System.out.println(stack);

Queue<Integer> queue = new ArrayDeque<>();
queue.offer(1); queue.offer(2); queue.offer(3);
System.out.println(queue);

Real output:

text
[3, 2, 1]
[1, 2, 3]

Same class, same three items, opposite order of printing: push inserts at the front, offer at the back. The way you talk to the object decides whether it acts as a stack or a queue.

Backend Insight

In backend code the choice shows up in small places that add up:

  • Result lists. A repository method that returns rows is usually a List (an ArrayList) because callers iterate and index it.
  • Work buffers. Request handlers and background workers use queue-like structures; ArrayDeque for a single-threaded buffer. Structures shared between threads need special care that this module does not cover; do not assume ArrayDeque is safe to share between threads.
  • Review conversations. "Why a LinkedList?" is a common code-review question. A good answer names the operation, not a vague feeling.

A common trap is looping over a LinkedList with for (int i = 0; i < list.size(); i++) list.get(i). Each get(i) walks, so the loop is O(n²). A for-each loop (which uses an iterator) visits each node once. Similarly, calling remove(0) repeatedly on an ArrayList as a queue is O(n²) overall.

Common Mistakes

Mistake 1: Choosing LinkedList "because insertion is fast"

Insertion is fast only at a position you already hold. Choosing it by default makes get(i) slow and memory use higher.

Mistake 2: Using ArrayList as a queue

remove(0) shifts everything. Use ArrayDeque.

Mistake 3: Using get(i) loops on a LinkedList

Use a for-each loop or an iterator instead, which is O(n) in total.

Mistake 4: Making speed claims without measuring

"This is faster" needs a benchmark on real data. Use Big O to choose, and measure before optimizing further.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. Which of the three classes has no get(int index) method?

Show solution A1

A1. ArrayDeque. It offers no index operations (calling get on a Deque does not compile).

A2. Which structure would you use by default for (a) a list of search results you display and index, (b) a stack of undo actions, (c) tasks waiting for a worker?

Show solution A2

A2. (a) ArrayList, (b) ArrayDeque used through Deque with push and pop, (c) ArrayDeque used through Queue with offer and poll.

A3. What is the cost of get(i) on an ArrayList and why?

Show solution A3

A3. O(1): the item address is computed from the start of the array and the index, as in Lesson 1.

Level B: Application

B1. A program adds 100,000 events to the front of a list one by one, and later reads them in order. Compare ArrayList and ArrayDeque for the adds, using Big O for the total.

Show solution B1

B1. ArrayList.add(0, x) shifts all existing items each time, so 100,000 adds cost about n²/2 item moves in total, O(n²). ArrayDeque.addFirst(x) is O(1) amortized, so the total is O(n). The ArrayDeque is the better fit, although reading by position later would require a different structure (or copying into a list at the end).

B2. A teammate wrote for (int i = 0; i < links.size(); i++) process(links.get(i)); where links is a LinkedList. Explain the cost and give the fix.

Show solution B2

B2. Each links.get(i) walks from the nearer end, so one call is O(n) and the loop is O(n²). Fix with a for-each loop (for (String link : links) process(link);), which goes through each node once, or use an ArrayList if indexing is truly needed.

B3. Fill in the decision for each: "random reads by position"; "oldest first"; "newest first"; "insert in the middle at a node you already hold".

Show solution B3

B3. Random reads by position: ArrayList. Oldest first: ArrayDeque as a queue. Newest first: ArrayDeque as a stack. Insert at a node already held: LinkedList (through an iterator), if you really have that situation.

Level C: Challenge

C1. A browser keeps the last 100 visited pages and needs to (a) add a visit, (b) go back, (c) forget the oldest when it holds more than 100. Choose one structure, and say which operations you need on each end.

Show solution C1

C1. An ArrayDeque, used as a deque. Add a visit with push (to the front). Go back with pop from the front. When the size exceeds 100, drop the oldest with pollLast (from the other end). Every operation touches an end, so all are O(1) amortized or better.

C2. A colleague says "I benchmarked LinkedList against ArrayList for inserting at the front, LinkedList won, so we should use it everywhere." List two things wrong with the conclusion.

Show solution C2

C2. First, one benchmark of one operation does not justify "everywhere": a LinkedList is slower at get(i), uses more memory per item, and is not the best queue or stack either. Second, hand-written benchmarks are easy to get wrong because of JVM warm-up, and ArrayDeque (not ArrayList) is the fair comparison for front operations; ArrayList front insertion was always going to lose.

Mini Challenge

Make a one-page cheat sheet with the table from this lesson, from memory, for the operations: get(i), add at end, add at front, remove from front, remove from middle. For each of the three classes write the cost and a six-word reason. Then add three one-line recommendations of the form "If ..., use ... because ...", for a result list, an undo stack, and a job queue.

Check your work: you gave ArrayList as the default for lists and ArrayDeque for both stack and queue, and every cost can be explained with arrays (shifting, copying) or nodes (walking, rewiring).

Summary & Next Lesson

Key takeaways

  • ArrayList: instant get(i), amortized O(1) append, O(n) at the front and in the middle. The default list.
  • LinkedList: O(1) at the ends and when relinking at a held position, O(n) get(i), extra memory per item. Rarely the best choice.
  • ArrayDeque: O(1) at both ends, no index access. The default stack and queue.
  • Choose by the operation you do most; use Big O to decide and measure before making speed claims.
  • Declare variables as List, Deque, or Queue and choose the class in one place.

Next lesson

Next is "Demo: building MyArrayList and watching it resize", in which you write a small dynamic array, see a +1 growth mistake, and fix it. It prepares the module's lab.

Self-check

  1. Which structure is the usual choice for a list you read by index?
  2. Which is the usual choice for a stack or queue?
  3. Why is get(i) on a LinkedList O(n)?
  4. Why does a get(i) loop over a LinkedList take O(n²)?
  5. What should you do before saying one structure is faster than another?
Show answers
  1. ArrayList.
  2. ArrayDeque.
  3. The nodes are scattered, so you must follow references from an end.
  4. There are n calls and each walks up to n nodes.
  5. Measure with a proper benchmark on realistic data, and also reason with Big O.

References

  • Oracle, Java SE 21 API, java.util.ArrayList. Supports: get and size constant time, add amortized constant time, other operations roughly linear.
  • Oracle, Java SE 21 API, java.util.LinkedList. Supports: a doubly linked list implementing List and Deque, index operations traverse from the nearer end, and null elements are allowed.
  • Oracle, Java SE 21 API, java.util.ArrayDeque. Supports: likely faster than Stack as a stack and LinkedList as a queue, and null elements prohibited.
  • Oracle, Java SE 21 API, java.util.Deque. Supports: use of a deque as a stack in preference to the legacy Stack.
  • Outputs and the compiler message were produced on JDK 21. The cost table for ArrayDeque (amortized O(1) at ends) and the claims about memory use, cache friendliness, and thread-safety come from general knowledge and the documentation summaries above; the exact costs of ArrayDeque operations were not read from a page that states them in Big O terms, so treat them as the standard expectation. Benchmark advice (JMH) is a general recommendation, not verified here.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.