Question 4 Β· Section 4

When to use ArrayList vs LinkedList?

4. ensureCapacity() for known size 5. Avoid LinkedList without good reason 6. Cache Locality > algorithmic complexity 7. GC pressure matters in collection choice

Language versions: English Russian Ukrainian

🟒 Junior Level

Short answer: In the vast majority of cases, use ArrayList.

Exceptions for LinkedList: (1) Removal via iterator in lists with 100k+ elements, where removeIf() (Java 8+) cannot be used. (2) Specialized data structures (LRU cache based on doubly-linked list).

// βœ… 99% of the time:
List<String> list = new ArrayList<>();

// ❌ LinkedList β€” don't use without a reason
List<String> list = new LinkedList<>();

The only reason for LinkedList:

  • Very large list (millions of elements)
  • And you need to remove elements via iterator during traversal

For queue/stack use ArrayDeque:

// ❌ LinkedList as queue
Deque<String> queue = new LinkedList<>();

// βœ… ArrayDeque (faster!)
Deque<String> queue = new ArrayDeque<>();

🟑 Middle Level

ArrayList: when to use

// βœ… Random access by index
list.get(100);  // O(1) β€” instant

// βœ… Iteration (traversing all elements)
for (String s : list) { ... }  // Fast (cache locality)

// βœ… Appending to end
list.add(e);  // O(1)* β€” amortized

// βœ… If size is known
ArrayList<String> list = new ArrayList<>(10000);
// β†’ No resizes

LinkedList: when (rarely) to use

// ⚠️ The only case:
// 1. Huge list (100,000+ elements)
// 2. Frequent removals VIA ITERATOR
// 3. During traversal

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (shouldRemove(it.next())) {
        it.remove();  // O(1) for LinkedList, O(n) for ArrayList
    }
}

// BUT! In Java 8+ better to use:
list.removeIf(this::shouldRemove);  // O(n) for both

ArrayDeque: for queues and stacks

// Stack (LIFO)
Deque<String> stack = new ArrayDeque<>();
stack.push("A");
stack.pop();

// Queue (FIFO)
Deque<String> queue = new ArrayDeque<>();
queue.offer("A");
queue.poll();

// β†’ Faster than LinkedList, less memory

Practical comparison

Scenario ArrayList LinkedList ArrayDeque
Many reads βœ… ❌ N/A
Appending βœ… βœ… βœ…
Insert at start ❌ βœ… βœ…
Delete by index ❌ ❌ N/A
Delete via iterator ❌ βœ… N/A
Memory βœ… ❌ βœ…
GC pressure βœ… ❌ βœ…

πŸ”΄ Senior Level

Cache Locality β€” the main argument

CPU Cache Lines: 64 bytes
Cache line β€” minimum memory block CPU loads from RAM to cache. Even if you need 1 byte, CPU loads all 64 bytes around it. So adjacent data in array is "free".

ArrayList:
  [ref1][ref2][ref3][ref4][ref5][ref6][ref7][ref8]
  β†’ One read β†’ 8 refs in L1 cache
  β†’ Next 7 elements = 0 latency!

LinkedList:
  [Node1]......[Node2]......[Node3]
     ↓             ↓             ↓
  RAM access: 100+ cycles EACH
  β†’ Cache miss on EVERY element!

System.arraycopy vs Node Creation

// ArrayList.add(0, e):
// β†’ System.arraycopy() β†’ SIMD instructions
// β†’ 1000 elements β†’ ~1 ΞΌs

// LinkedList.add(0, e):
// β†’ new Node() β†’ heap allocation
// β†’ 3 refs (item, next, prev)
// β†’ 1000 elements β†’ ~5 ΞΌs + GC overhead

// ArrayList is faster up to ~10-20k elements!
// (Based on JMH benchmarks on modern JVMs. Exact threshold depends on CPU, JVM version and access pattern.)

GC Pressure Deep Dive

LinkedList: 1M elements
  β†’ 1M Node objects
  β†’ 32 MB overhead only
  β†’ Minor GC: scan 1M objects
  β†’ STW pauses!

ArrayList: 1M elements
  β†’ 1 Object[] array
  β†’ 4 MB
  β†’ GC: one ref β†’ instantly

removeIf optimization

// ❌ Manual removal via iterator
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
    if (condition(it.next())) {
        it.remove();  // O(n) for ArrayList (shift each time)
    }
}

// βœ… removeIf (Java 8+)
list.removeIf(e -> condition(e));
// β†’ First finds all to remove (O(n))
// β†’ Then ONE shift (O(n))
// β†’ Total: O(n) instead of O(nΒ²)!

When ArrayList also loses

// Frequent insertions at START of large list (> 100k elements)
// ArrayList: shift 100k elements each time

// Solution:
// 1. ArrayDeque (if insertion at both ends)
// 2. Or two ArrayLists (head + tail)
// 3. Or rethink architecture

Production Experience

Real case #1: LinkedList β†’ ArrayList

  • Log: 500,000 lines in LinkedList
  • Memory: 800 MB
  • Iteration: 5 seconds
  • Solution: ArrayList
  • Result: 80 MB, 100 ms (50x speedup!)

Real case #2: LinkedList as queue

  • Message queue: LinkedList
  • GC: every 2 seconds (Node creation)
  • Solution: ArrayDeque
  • Result: GC once per minute

Best Practices

  1. ArrayList β€” 99% of cases
  2. ArrayDeque β€” queues/stacks
  3. removeIf() instead of iteration with removal
  4. ensureCapacity() for known size
  5. Avoid LinkedList without good reason
  6. Cache Locality > algorithmic complexity
  7. GC pressure matters in collection choice

Summary for Senior

  • ArrayList β€” default choice (cache locality)
  • LinkedList β€” anti-pattern for modern CPUs
  • ArrayDeque β€” for stacks and queues
  • removeIf() > iteration with removal
  • Cache Locality > Big O in practice
  • GC Pressure β€” hidden cost of LinkedList
  • System.arraycopy faster than creating Node
  • Valhalla (future): Inline Types will widen the gap

🎯 Interview Cheat Sheet

Must know:

  • ArrayList β€” default choice for 99% scenarios
  • Cache locality > algorithmic complexity: CPU loads 64-byte cache line at once, getting 8-16 refs β€œfor free”
  • System.arraycopy() = SIMD instructions β€” faster than new Node() for lists up to 10-20k elements
  • removeIf() is better than manual iterator removal: one pass + one shift = O(n) instead of O(nΒ²)
  • ArrayDeque β€” for stacks and queues (faster than LinkedList, less memory, no GC pressure)
  • GC Pressure: LinkedList 1M elements = 32 MB garbage, ArrayList = 0 new objects
  • ensureCapacity() saves 30+ resizes when size is known
  • Real case: replacing LinkedList β†’ ArrayList gave 50-200x speedup and 10x memory reduction

Common follow-up questions:

  • When can LinkedList win? β€” Frequent insertions at START of list >100k elements; removal via iterator in huge lists
  • What is cache line and why it matters? β€” 64-byte block, CPU loads it entirely; ArrayList uses it, LinkedList β€” no
  • Why removeIf() faster than iterator loop? β€” First finds all elements to remove, then ONE array shift
  • When does ArrayList also lose? β€” Frequent insertions at start of large list (>100k) β€” then ArrayDeque

Red flags (NOT to say):

  • ❌ β€œLinkedList is better for frequent insertions” β€” ArrayList is faster up to 10-20k elements thanks to SIMD
  • ❌ β€œUse LinkedList for queue” β€” ArrayDeque is always better (faster, less memory)
  • ❌ β€œBig O determines performance” β€” cache locality and constants matter more in practice
  • ❌ β€œIterator removal is normal practice” β€” use removeIf() in Java 8+

Related topics:

  • [[What is the difference between ArrayList and LinkedList]]
  • [[What is the time complexity of operations in ArrayList]]
  • [[What is the time complexity of operations in LinkedList]]
  • [[What is Deque]]
  • [[What is Vector and how does it differ from ArrayList]]