Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 11

ArrayList vs. LinkedList: Performance Trade-offs

Recommended resources

Hello! Welcome back to our module on Java Collections & Problem-Solving.

In our last lesson, we compared different Map implementations, establishing a key principle for interview preparation: choosing the right data structure involves analyzing trade-offs in performance, ordering, and thread-safety.

Today, we're applying that same critical lens to another fundamental interview topic: the List interface. We will move beyond textbook definitions to explore why the performance of ArrayList and LinkedList in the real world can sometimes be counter-intuitive. By the end of this lesson, you will be able to analyze the performance trade-offs between ArrayList and LinkedList for common operations, preparing you to provide nuanced, in-depth answers in a technical interview.

1. The Core Difference: Internal Structure

The performance characteristics of ArrayList and LinkedList stem directly from their underlying data structures.

  • ArrayList: Internally, it uses a dynamic array to store elements. This means elements are stored in a contiguous block of memory, just like a standard array. When it runs out of space, it allocates a new, larger array and copies the old elements over.
  • LinkedList: This class is implemented as a doubly-linked list. Each element is a Node object that holds the actual element, a pointer to the previous Node, and a pointer to the next Node. These nodes can be scattered anywhere in memory.

This structural difference is the root cause of all their performance trade-offs.

Java ArrayList vs. LinkedList Internal Representation
This image visually contrasts the contiguous memory block of an `ArrayList` with the interconnected `Node` objects of a `LinkedList`. This is the key to understanding their different performance behaviors.

2. Theoretical Performance: The Big O Notation

Before we dive into real-world benchmarks, let's review the classic time complexity you'd find in a textbook. This table gives a high-level overview.

ArrayList vs. LinkedList Operations Complexity Comparison
This table shows the theoretical time complexity for common operations. Note how `get` is fast for `ArrayList` (O(1)), while operations at the ends of the list are fast for `LinkedList` (O(1)).

While these Big O values are a good starting point, they don't tell the whole story. In a senior-level interview, you're expected to go deeper. Why is an O(n) operation sometimes faster in practice than a different O(n) operation? Let's find out.


3. Practical Performance: Measure, Don't Guess

The principle "Measure, Don't Guess" is paramount in performance tuning. Theoretical complexity can be misleading due to factors like CPU architecture, caching, and JVM optimizations.

The best way to understand the real-world trade-offs is to see them in action through benchmarks. The following video from the official Java channel uses the Java Microbenchmark Harness (JMH) to provide a precise comparison. We'll go through its key findings.

Choosing between ArrayList and LinkedList - JEP Cafe #20

To start, watch this introductory segment which frames the ArrayList vs. LinkedList debate and introduces the importance of precise benchmarking over outdated assumptions.

Watch from the beginning until 01:50. This will set the stage for our analysis.

3.1. Element Access: get(index)

This is where the difference is most stark.

  • ArrayList: Accessing an element by index (list.get(500)) is an O(1) operation. Since the data is in a contiguous array, the JVM can do simple math to calculate the memory address of any element and jump straight to it. This is called random access.
  • LinkedList: Accessing an element by index is O(n). There are no shortcuts. To get the 500th element, the code must start at the head (or tail, whichever is closer) and traverse through 500 Node objects one by one.

This traversal in LinkedList introduces a major performance killer on modern hardware: pointer chasing. Because each Node can be in a different part of memory, the CPU often can't predict what memory it will need next. This leads to cache misses, where the CPU has to wait for data to be fetched from slow main memory instead of a fast CPU cache. ArrayList benefits from spatial locality; because its elements are side-by-side in memory, when the CPU fetches one element, it often pulls subsequent elements into the cache for free, making iteration and access much faster.

Choosing between ArrayList and LinkedList - JEP Cafe #20

Now, let's see the benchmark results for accessing elements. Pay close attention to the explanation of 'pointer chasing' and how it dramatically impacts performance when accessing the middle of a LinkedList.

Watch from 06:27 to 11:45. Notice how ArrayList performance is constant regardless of position, while LinkedList performance degrades significantly as it moves toward the middle of the list.

3.2. Iteration

What if you just need to loop through all the elements?

  • ArrayList is again faster here, thanks to spatial locality. The CPU can efficiently pre-fetch the data, leading to fewer cache misses.
  • LinkedList is slower due to pointer chasing.
  • A common pitfall: Never iterate over a LinkedList using an index-based for loop (for (int i = 0; i < list.size(); i++)). This is an O(n²) operation! For each iteration i, the loop starts from the beginning and traverses i nodes to get the element. Always use an enhanced for-each loop or an explicit Iterator.

Choosing between ArrayList and LinkedList - JEP Cafe #20

This next segment benchmarks iteration. It starkly illustrates the O(n²) disaster of using an index-based loop on a LinkedList and compares the more optimized iterator approach.

Watch from 14:21 to 16:50. The key takeaway is to always use an iterator for LinkedList and to understand why ArrayList is still faster even then.

3.3. Insertion and Deletion: add(index, element) / remove(index)

This is the classic scenario where LinkedList is supposed to shine.

  • LinkedList: Adding or removing an element requires just updating a few pointers (the next of the previous node and the prev of the next node). This is an O(1) action. However, you first have to find the node, which is an O(n) traversal. So the total operation is O(n).
  • ArrayList: Adding or removing an element in the middle requires shifting all subsequent elements. This is done via System.arraycopy(), a highly optimized native operation. This is also an O(n) operation.

So, it's a battle of two O(n) operations: pointer traversal vs. memory copy. Which one wins?

Surprisingly, ArrayList often wins or is very competitive. System.arraycopy() is extremely fast, while the pointer chasing to find the insertion point in a LinkedList is very slow.

The one area where LinkedList is the undisputed champion is adding or removing elements at the very beginning or end of the list.

  • list.addFirst() / list.removeFirst()
  • list.addLast() / list.removeLast()

These are true O(1) operations for LinkedList because it maintains direct pointers to the head and tail. For ArrayList, adding/removing at the end is amortized O(1), but adding/removing at the beginning is O(n) because all other elements must be shifted.

This makes LinkedList an excellent implementation for Queues and Deques (double-ended queues).

Choosing between ArrayList and LinkedList - JEP Cafe #20

Let's examine the benchmark results for insertions. This segment reveals that ArrayList's performance is often better than expected, and highlights the specific use case where LinkedList truly excels.

Watch from 18:13 to 23:25. Focus on how ArrayList's resizing cost compares to LinkedList's insertion cost, and the conclusion that LinkedList is best suited for stack/queue operations.


4. Memory Consumption

There's also a memory trade-off to consider.

  • LinkedList has a higher memory overhead per element. Each element is wrapped in a Node object which stores not just your data, but also two references (previous and next) and the object header itself. This adds up to about 24 bytes of overhead per element on a typical 64-bit JVM.
  • ArrayList is much more memory-efficient. It stores only the references to your objects in its internal array. However, an ArrayList might have unused capacity at the end of its array, wasting some space.
  • Important note: An ArrayList never shrinks automatically. If you have an ArrayList with 1 million elements and you remove() 999,999 of them, it will still hold a massive internal array. You can manually release this memory by calling list.trimToSize().

Choosing between ArrayList and LinkedList - JEP Cafe #20

Finally, let's analyze memory usage. This is a crucial and often overlooked aspect of the comparison.

Watch from 23:25 to 27:55. Understand why LinkedList consumes significantly more memory and the implications of ArrayList's trimToSize() method.


5. Summary and Interview Guide

So, when do you choose one over the other?

Feature ArrayList LinkedList
Best For General-purpose list, frequent random access. The default choice. Implementing a Queue or Deque (frequent add/remove at ends).
get(index) O(1) - Very fast O(n) - Very slow
add/remove (middle) O(n) - Faster than you'd think (System.arraycopy) O(n) - Slower than you'd think (pointer traversal)
add/remove (ends) O(n) at start, amortized O(1) at end O(1) - Very fast
Memory Usage Lower overhead Higher overhead per element

The key takeaway for an interview: Unless your primary use case is adding and removing from the head of the list (acting as a queue), ArrayList is almost always the better choice due to its superior random access, iteration speed, and memory efficiency. The classic textbook advice that LinkedList is better for insertions/deletions is generally misleading on modern hardware except for the specific case of operating at the ends.

Test your understanding!

For each of the following scenarios, which List implementation would you choose and why?

  1. You need to store a list of 10,000 user objects and will frequently need to retrieve the Nth user.
  2. You are implementing a job scheduler. Jobs are added to the end of a queue and processed from the front. The number of jobs can fluctuate greatly.
  3. You are storing the path taken by a user through a website, adding each new page to the end of the list. The list is read only once at the end of the session.
  4. You are managing a playlist where songs can be inserted or removed at any position. The list size is typically small (under 100 songs).
Show answer
  1. ArrayList. The requirement for frequent retrieval of the Nth user screams for the O(1) random access that ArrayList provides. LinkedList would be unacceptably slow here.
  2. LinkedList. This is a classic First-In, First-Out (FIFO) queue. LinkedList excels at this, providing true O(1) performance for adding to the end (addLast) and removing from the front (removeFirst).
  3. ArrayList. While you are only adding to the end (which both lists do well), the overall better memory efficiency and faster iteration speed of ArrayList make it the superior choice. There is no operation here that specifically favors LinkedList.
  4. ArrayList. This is a trick question. While LinkedList is theoretically better for middle insertions/deletions, we learned that for small lists, the performance difference is negligible, and the cost of pointer chasing often makes ArrayList's System.arraycopy faster anyway. Given its better memory profile and access speed, ArrayList remains the pragmatic choice.

Conclusion

Congratulations! You've moved beyond a surface-level understanding of ArrayList and LinkedList. You can now analyze their performance not just in terms of Big O notation, but also in the context of modern hardware and JVM behavior—a hallmark of a senior engineer.

Key Takeaways:

  • ArrayList's contiguous memory gives it excellent spatial locality, making random access and iteration very fast. It's your default List.
  • LinkedList's node-based structure requires pointer chasing for most operations, which is slow due to CPU cache misses.
  • The primary strength of LinkedList is its true O(1) performance for adding/removing at the head or tail, making it ideal for Queue implementations.
  • ArrayList is significantly more memory-efficient than LinkedList.

In our next lesson, we will begin applying this knowledge to solve common interview problems. We'll start with a classic: "Solve the 'Two Sum' problem and its variations using a HashMap for efficient lookups."