Nine out of ten lists you create in real Java projects will be ArrayLists. That is exactly why interviewers probe it: "How does ArrayList grow?", "Why is add amortized O(1)?", "What happens if you remove while iterating?" If you can answer those three questions with mechanics rather than memorized lines, you stand out. Let's build that understanding.
ArrayList is one implementation of the List interface, so everything Lists guarantee — insertion order, duplicates, index access — applies here.
The internal structure: one array and a size counter
Strip away the API and ArrayList is just two fields:
transient Object[] elementData; // the backing array
private int size; // how many slots are actually used
Every behavior follows from this. get(i) returns elementData[i] after a bounds check —
that is why random access is O(1). add(x) writes to elementData[size] and increments
size. The interesting part is what happens when the array is full.
Two related numbers matter and are easy to confuse:
- Size — elements stored. What
size()returns. - Capacity — length of
elementData. Invisible through the public API.
new ArrayList<>() does not even allocate the array immediately. It points to a shared
empty array, and only on the first add does it allocate the default capacity of 10.
That lazy allocation keeps thousands of empty lists cheap.
How growth works: the 1.5x rule
When add finds the array full, ArrayList grows it:
- Compute new capacity:
oldCapacity + (oldCapacity >> 1)— that is, 1.5x the old. - Allocate the new array and copy all elements with
Arrays.copyOf. - Point
elementDataat the new array and complete the add.
So a default list grows 10 → 15 → 22 → 33 → 49 → 73 and so on. Each resize is O(n), but because capacity grows geometrically, resizes become exponentially rarer. Spread the copy cost over all the cheap adds between resizes and the average cost per add is constant — that is precisely what amortized O(1) means.
Interview note: Vector doubles its capacity (2x) on resize while ArrayList grows by 1.5x. Mentioning this contrast when asked about ArrayList growth signals that you have actually looked at how the classes behave, not just read a definition.
You can watch the resize behavior indirectly by timing bulk adds with and without a pre-sized list:
import java.util.ArrayList;
import java.util.List;
public class GrowthDemo {
static long fill(List<Integer> list, int n) {
long start = System.nanoTime();
for (int i = 0; i < n; i++) {
list.add(i);
}
return (System.nanoTime() - start) / 1_000_000; // ms
}
public static void main(String[] args) {
int n = 5_000_000;
long grown = fill(new ArrayList<>(), n); // ~23 resizes on the way
long presized = fill(new ArrayList<>(n), n); // zero resizes
System.out.println("Default capacity : " + grown + " ms");
System.out.println("Pre-sized : " + presized + " ms");
}
}
Run it a few times; the pre-sized version is consistently faster because it skips every intermediate allocation and copy. The exact numbers vary by machine — what matters is the pattern.
Pro tip: When you know the final size in advance — loading query results, converting another collection — pass it to the constructor:
new ArrayList<>(expectedSize). It is a one-line change that eliminates all resize copies.
What every operation costs
| Operation | Complexity | Why |
|---|---|---|
get(i) / set(i, x) |
O(1) | Direct array indexing |
add(x) at end |
Amortized O(1) | Occasional resize copy, rare by design |
add(i, x) |
O(n) | System.arraycopy shifts elements right |
remove(i) |
O(n) | Shifts elements left to close the gap |
remove(Object) |
O(n) | Linear search, then shift |
contains(x) / indexOf(x) |
O(n) | Linear scan calling equals |
| Iteration | O(n) | Sequential array walk, very cache-friendly |
The O(n) rows share one cause: arrays are contiguous, so inserting or deleting anywhere except the tail means physically moving everything after that position. Removing index 0 of a million-element list moves 999,999 references. If your workload does that in a loop, reconsider the data structure — the trade-offs are laid out in the ArrayList vs LinkedList comparison linked at the end of this page.
Core operations in practice
A runnable tour of the operations you will actually use, with the gotchas flagged:
import java.util.ArrayList;
import java.util.List;
public class ArrayListOps {
public static void main(String[] args) {
List<Integer> marks = new ArrayList<>(List.of(85, 42, 91, 42, 77));
marks.add(60); // append -> [85, 42, 91, 42, 77, 60]
marks.add(0, 99); // insert at head, shifts all right
marks.set(1, 80); // replace index 1 (was 85)
// remove by INDEX vs remove by VALUE — the classic Integer trap
marks.remove(2); // removes element AT index 2
marks.remove(Integer.valueOf(42)); // removes first VALUE 42
System.out.println(marks); // [99, 80, 42, 77, 60]
System.out.println(marks.indexOf(42)); // 2
System.out.println(marks.contains(91)); // false
marks.removeIf(m -> m < 70); // bulk conditional removal
System.out.println(marks); // [99, 80, 77]
}
}
Two things to notice: the remove overload trap with boxed Integers, and removeIf,
which handles remove-while-filtering in a single safe call.
Fail-fast iteration and ConcurrentModificationException
ArrayList keeps a modCount field that increments on every structural change (add,
remove, clear). Its iterator snapshots that count when created and compares on every
next(). If the counts differ — someone modified the list outside the iterator — it
throws ConcurrentModificationException immediately rather than returning corrupt data.
List<String> tools = new ArrayList<>(List.of("Git", "Maven", "Docker"));
for (String t : tools) {
if (t.equals("Maven")) {
tools.remove(t); // throws ConcurrentModificationException
}
}
The fix is one of: Iterator.remove(), removeIf(...), or collecting survivors into a
new list. This exact scenario is a staple of
Java collections interview questions because it separates
people who have debugged real code from people who have only read syntax.
Common mistake: A common mistake beginners make is assuming
ConcurrentModificationExceptiononly happens with multiple threads. The name misleads: a single thread modifying a list mid-iteration triggers it too, as the loop above shows.
Memory behavior worth knowing
- ArrayList stores object references, not primitives. A
List<Integer>holds pointers to boxed Integer objects, which costs more memory and indirection than anint[]. For large numeric datasets, prefer arrays or specialized libraries. - Capacity never shrinks on its own. After removing 990,000 of 1,000,000 elements, the
backing array still has ~1,000,000 slots. Call
trimToSize()if that memory matters. - Because elements are contiguous, iteration enjoys excellent CPU cache locality — a big reason ArrayList outperforms LinkedList even at jobs that look linked-list-shaped.
Thread safety: what to use instead
ArrayList performs zero synchronization. If two threads add concurrently, you can lose
elements or see an ArrayIndexOutOfBoundsException from a torn resize. Your options:
Collections.synchronizedList(new ArrayList<>())— wraps every method in a lock; you must still synchronize manually while iterating.CopyOnWriteArrayList— copies the whole array on each write; excellent for read-heavy, write-rare workloads like listener lists, terrible for frequent writes.
Vector is the third, legacy option and is not recommended for new code.
Where to go next
You now know the mechanics that generate every ArrayList answer: a contiguous array, geometric growth, shift-on-insert, fail-fast iteration. Next, put it in context — compare it head-to-head in ArrayList vs LinkedList, see how hashing changes the game in HashSet, or step back to the full collections map. If you want this depth across all of core Java with mentor-led practice, that is what our Java Full Stack program is built for.
Frequently Asked Questions
How does ArrayList work internally in Java?
What is the default initial capacity of an ArrayList?
Why is adding to an ArrayList called amortized O(1)?
Is ArrayList thread-safe?
What is the difference between size and capacity in ArrayList?
When should I not use an ArrayList?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — Explore the Program

