The Collections Framework

ArrayList in Depth

15 min Lesson 2 of 14

ArrayList in Depth

ArrayList is the most frequently used collection in Java. It provides a resizable array backed by a plain Object[] under the hood, giving you fast random access by index while freeing you from managing array growth manually. Understanding how it works — not just which methods to call — makes you a far better engineer.

Creating an ArrayList

You almost always program to the List interface and assign an ArrayList instance. This keeps your code flexible: if you later need a LinkedList, only the constructor changes.

import java.util.ArrayList; import java.util.List; // preferred: declare as List, instantiate as ArrayList List<String> languages = new ArrayList<>(); // if you know the rough size upfront, pass an initial capacity List<Integer> scores = new ArrayList<>(50);
Use the interface type on the left. Writing List<String> list = new ArrayList<>() instead of ArrayList<String> list = new ArrayList<>() lets you swap the implementation later without touching call sites.

The Four Core Mutation Methods

add, set, remove, and get are the daily workhorses.

List<String> fruits = new ArrayList<>(); // add(E e) — appends to the end, O(1) amortised fruits.add("apple"); fruits.add("banana"); fruits.add("cherry"); // ["apple", "banana", "cherry"] // add(int index, E e) — inserts at position, O(n) because elements shift right fruits.add(1, "blueberry"); // ["apple", "blueberry", "banana", "cherry"] // get(int index) — retrieves by index, O(1) String first = fruits.get(0); // "apple" // set(int index, E e) — replaces at index, O(1), returns old value String old = fruits.set(2, "mango"); // old = "banana" // ["apple", "blueberry", "mango", "cherry"] // remove(int index) — removes by position, O(n) due to shifting fruits.remove(0); // ["blueberry", "mango", "cherry"] // remove(Object o) — removes first occurrence by value, O(n) fruits.remove("mango"); // ["blueberry", "cherry"]
Two flavours of remove for integers. With a List<Integer>, list.remove(2) removes the element at index 2, not the value 2. To remove by value, box it: list.remove(Integer.valueOf(2)).

How Dynamic Resizing Works

An ArrayList starts with an internal array of capacity 10 (the default). When you add an element and the array is full, Java allocates a new array roughly 1.5× larger, copies all existing elements, and discards the old array. This copy is O(n) but happens infrequently, so the amortised cost of add is still O(1).

// Force the list to grow several times List<Integer> numbers = new ArrayList<>(); // internal capacity = 10 for (int i = 0; i < 100; i++) { numbers.add(i); // triggers resizes at ~10, ~15, ~22, ~33 ... until capacity > 100 } System.out.println(numbers.size()); // 100 // If you know the final size, pre-size to avoid resizes entirely List<Integer> preSized = new ArrayList<>(100);

Useful Utility Methods

List<String> items = new ArrayList<>(List.of("x", "y", "z", "y")); int sz = items.size(); // 4 boolean empty = items.isEmpty(); // false boolean has = items.contains("y"); // true int idx = items.indexOf("y"); // 1 (first occurrence) int last = items.lastIndexOf("y"); // 3 (last occurrence) items.clear(); // removes all elements System.out.println(items.isEmpty()); // true

Iterating an ArrayList

There are three idiomatic ways to iterate. The enhanced for-loop is cleanest for read-only passes; the index loop is needed when you need the position; forEach with a lambda is expressive for simple actions.

List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Carol")); // 1. Enhanced for-loop — clean, read-only for (String name : names) { System.out.println(name); } // 2. Index loop — when you need the position for (int i = 0; i < names.size(); i++) { System.out.println(i + ": " + names.get(i)); } // 3. forEach with lambda — concise for simple operations names.forEach(name -> System.out.println(name.toUpperCase()));
Never remove elements while iterating with a for-each loop. It throws a ConcurrentModificationException. Use an explicit Iterator and call iterator.remove(), or collect what to remove first and call list.removeAll(toRemove).

Sublist and Bulk Operations

List<Integer> nums = new ArrayList<>(List.of(10, 20, 30, 40, 50)); // subList returns a VIEW — changes affect the original List<Integer> middle = nums.subList(1, 4); // [20, 30, 40] middle.clear(); // nums is now [10, 50] // addAll — append another collection in one call List<String> a = new ArrayList<>(List.of("one", "two")); List<String> b = List.of("three", "four"); a.addAll(b); // ["one", "two", "three", "four"] // removeIf — remove all elements matching a predicate (Java 8+) a.removeIf(s -> s.startsWith("t")); // ["one", "four"]

When to Use ArrayList

Choose ArrayList when:

  • You need fast indexed accessget(i) is O(1).
  • You mostly append to the endadd(e) is O(1) amortised.
  • Reads far outnumber writes, or random access is frequent.

Consider alternatives when:

  • You insert/delete frequently in the middle — shifting is O(n); a LinkedList or Deque may be faster depending on access patterns.
  • You need thread safety — use Collections.synchronizedList or CopyOnWriteArrayList.
  • You need uniqueness — use a Set instead.

Summary

ArrayList combines the O(1) random access of an array with automatic growth. Know your complexity: get and end-add are O(1); insertions and deletions in the middle are O(n). Declare your variable as List, pre-size when you know the capacity, and avoid mutating a list inside a for-each loop.