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 access —
get(i) is O(1).
- You mostly append to the end —
add(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.