🗃️ Collections Framework · Intermediate

ArrayList in Java

Resizable array: fast get, amortized O(1) add, slow middle insert.

🧩 The mysteryYour list just ran out of space. Java doesn't crash: it quietly builds a bigger home and moves every element over. How can that possibly be fast?

A row of numbered lockers

An **ArrayList stores elements in an internal array**. Element i lives at a computed position, so get(i) and set(i, x) jump straight there: O(1), no walking.

List<String> list = new ArrayList<>();
list.add("A");        // [A]
list.add("C");        // [A, C]
list.get(1);          // "C", instantly

Inserting shifts everyone

add(i, x) doesn't overwrite. It shifts every later element one slot right to make room, so inserting or removing in the middle costs O(n).

list.add(1, "B");     // [A, B, C]
// "C" moved from index 1 to index 2
list.remove(0);       // [B, C], all shift left
🔮 Predict it

Your turn

What does this print?

List<String> list = new ArrayList<>();
list.add("x");
list.add("z");
list.add(0, "w");
System.out.println(list + " " + list.size());
  1. [x, w, z] 3
  2. [w, x, z] 3
  3. [w, z] 2
Show the answer

add(0, "w") inserts at the front and pushes x and z right. Nothing is replaced, so the size grows to 3.

When the lockers run out

When the array is full, ArrayList allocates one about 1.5× bigger and copies everything. That copy is O(n), but it gets rarer as the list grows. Averaged over all adds, appending stays constant: amortized O(1).

// capacity 10 → 15 → 22 → 33 → ...
for (int i = 0; i < 1_000; i++) {
    list.add("item");  // rare copies
}
🔮 Predict it

Capacity is not size

This list has room for 100 elements. What happens?

List<Integer> list = new ArrayList<>(100);
System.out.println(list.get(0));
  1. Prints null
  2. Prints 0
  3. Throws IndexOutOfBoundsException
Show the answer

**new ArrayList<>(100) sets the capacity (internal storage), not the size. The list is still empty**, so index 0 is out of bounds. Valid indexes go from 0 to size() - 1.

Building a list in reverse

✗ O(n²)
for (String s : input) {
    list.add(0, s); // shifts all
}

Every front insert shifts the whole list. A million items means about a trillion moves.

✓ O(n)
for (String s : input) {
    deque.addFirst(s); // ArrayDeque
}

ArrayDeque.addFirst is O(1). Or append to an ArrayList and call Collections.reverse once.

💼 In the real world

Where it bites

ArrayList is the default list in almost every Java codebase. Two habits pay off: pass a capacity when you know the size (new ArrayList<>(n)) to skip the resize copies, and never insert at index 0 in a loop. That pattern has turned plenty of import jobs from seconds into hours.

Key takeaways

  1. get(i) and set(i, x): O(1)
  2. add(x) at the end: amortized O(1) — occasional resize copies
  3. add(i, x) / remove(i) in the middle: O(n) because elements shift
  4. Capacity (internal array length) is not the same as size()

💡 An ArrayList is a row of numbered lockers: you can walk straight to locker 57, but squeezing a new locker in the middle means moving every locker after it.

🤯 Did you know?

In modern JDKs, new ArrayList<>() doesn't allocate its first 10-slot array until you add something. All empty ArrayLists share one empty array, which saves memory when you create many lists that stay empty.

Practice questions

What does this print?

List<String> list = new ArrayList<>();
list.add("A");
list.add("C");
list.add(1, "B");
System.out.println(list + " " + list.size());
  1. [A, C, B] 3
  2. [A, B, C] 3
  3. [B, A, C] 3
  4. [A, B, C] 2
Check your answer

[A, B, C] 3. add(1, "B") inserts at index 1 and shifts "C" one place right. Nothing is overwritten, so the size becomes 3.

Why is ArrayList.add(x) at the end described as *amortized* O(1)?

  1. Every add copies the whole array, but the JIT hides the cost
  2. It's O(1) only if you passed an initial capacity
  3. Occasionally the full array must be copied into a bigger one, but that's rare enough that the average cost per add stays constant
  4. Adding is always exactly O(1); 'amortized' is just a label
Check your answer

Occasionally the full array must be copied into a bigger one, but that's rare enough that the average cost per add stays constant. Because the array grows by a factor (~1.5×), resizes become rarer as the list grows. Spread across all adds, the copying averages out to a constant cost.

Next: LinkedList promises O(1) inserts anywhere. So why does its own author say he never uses it?