Java ArrayList
Use the resizable-array-backed ArrayList class for fast random-access lists in Java.
Java ArrayList
ArrayList<E> is the workhorse List implementation. Internally it's a plain Java array (Object[]) with a length counter and some growth logic on top. That gives it the performance shape most code wants: O(1) random access, amortised O(1) append at the end, and only the occasional pause when the backing array has to grow. When someone says "use a list" without specifying which, they almost always mean ArrayList. This chapter explains why, what the trade-offs are, and the handful of methods unique to the class.
What's inside
An ArrayList holds three pieces of state:
Object[] elementData— the backing array. Bigger thansizeby some slack.int size— how many of those slots are in use.int modCount— a counter that fails iterators if you mutate while iterating.
The Javadoc is precise about complexity: size, isEmpty, get, set, iterator, listIterator, and add (at the end) all run in constant time. Insert / remove anywhere else is linear because the tail of the array has to slide. We'll come back to that.
Creating one
List<String> a = new ArrayList<>(); // default capacity (10)
List<String> b = new ArrayList<>(1_000_000); // pre-size — avoids re-grows
List<String> c = new ArrayList<>(otherList); // copy of any Collection
List<String> d = new ArrayList<>(List.of(\"a\", \"b\", \"c\")); // from a small literalIf you know roughly how many elements you're going to add, pass the capacity to the constructor. The default capacity is 10, and every time the array fills up it grows by about 50% — which means a lot of allocations and copies if you append millions of items starting from the default. One-line fix:
List<Row> rows = new ArrayList<>(expectedSize);For "from this fixed set," prefer the List.of(...) factory (covered later in this chapter) — it's smaller and immutable.
Adding and removing — the cost depends on where
Every List.add(E) and add(int, E) and remove(int) and remove(Object) is O(n) in the worst case if it touches the middle of the array. The reason is mechanical: an array is contiguous memory, so inserting at the front means every existing element has to move one slot to the right. The amortised cost of appending at the end is O(1) (no shifting), but every other position is linear in the number of elements after the insertion point.
In practice that means:
- Building a list by repeated
list.add(x)→ fast, regardless of size. Append-at-end is whatArrayListis for. - Calling
list.add(0, x)in a loop → quadratic. Don't. - Calling
list.remove(0)repeatedly to drain → quadratic. Use aDequeor iterate in reverse.
If you find yourself constantly inserting or removing at the front, ArrayDeque is the right tool.
Capacity vs. size
These are two different numbers:
size()is the public, contract-level count of elements.- The capacity — the length of the backing array — is internal and bigger than
size.
When the slack runs out, ArrayList allocates a new array roughly 1.5× the old length and copies. That's the "growth" the Javadoc warns about. Two control levers:
ensureCapacity(int)— bump the backing array to at least that length before a known burst ofaddcalls.trimToSize()— shrink the backing array to exactlysize. Useful once you know the list won't grow further (e.g., before caching it for a long time).
Neither is something you'll reach for often, but it's worth knowing they exist when you're tuning a hot path.
ArrayList is not thread-safe
Two threads modifying the same ArrayList will eventually corrupt it. There is no synchronisation, no atomic operation, nothing. If you need concurrent access, your options are:
Collections.synchronizedList(new ArrayList<>())— wraps every mutator in a lock. Iterators still aren't safe — you have to synchronise externally during iteration. Fine for low-contention cases.CopyOnWriteArrayList— every mutation allocates a new copy of the backing array. Iteration is lock-free and sees a frozen snapshot. Brilliant for "many readers, occasional writer" (event listeners, config caches). Awful for write-heavy workloads.Vector— also synchronised, but a 1995 design with the same weaknesses assynchronizedList. Don't pick it for new code.
Threading is its own deep topic; for now, the rule is: an ArrayList shared across threads needs a wrapper or a different class.
Iteration and ConcurrentModificationException
Adding or removing from an ArrayList while iterating over it through the for-each loop almost always throws ConcurrentModificationException:
List<Integer> xs = new ArrayList<>(List.of(1, 2, 3, 4));
for (int x : xs) {
if (x % 2 == 0) xs.remove(Integer.valueOf(x)); // throws
}The fail-fast check compares the iterator's snapshot of modCount against the list's current modCount. The two ways to mutate safely:
xs.removeIf(x -> x % 2 == 0); // 1. predicate form — cleanest
Iterator<Integer> it = xs.iterator();
while (it.hasNext()) // 2. explicit Iterator.remove()
if (it.next() % 2 == 0) it.remove();removeIf is the modern default. Reach for the explicit iterator only when the condition depends on state you don't want to compute twice.
Useful methods you didn't see on List
ArrayList adds a few methods the List interface doesn't have:
void trimToSize()— shrink to fit.void ensureCapacity(int)— pre-grow.Object clone()— shallow copy. Equivalent tonew ArrayList<>(this)and almost never used.
That's it. Almost everything you'll call on an ArrayList is from the List interface.
A worked example: ArrayList in motion
The program below builds an ArrayList, exercises its index-based operations, drains it, and finishes with the capacity-pre-grow vs. default-grow timing difference so you can see the cost of forgetting to pre-size.
The order of operations to read out of the output:
fruits.add(1, \"avocado\")slid every later element one slot to the right. For four elements that's invisible; for a million it would dominate the runtime.removeIfandsubListare the two pieces of machinery that make most realArrayListcode clean — predicate-based bulk removal and live-window operations.- The timing block is illustrative, not benchmark-grade, but the gap is real. Default-capacity append re-grows the backing array about 30 times by the millionth element; pre-sizing eliminates every one of those copies.
What's next
ArrayList is the right answer almost always. The interesting case is when it isn't — heavy insertion and removal at the front or middle of a long list. That's the niche of LinkedList: same List interface, completely different storage. Same problem, two answers — and we'll measure when each wins.
Practice
You're about to read a CSV file of 5 million rows into a `List<Row>` by appending one row at a time. Which `ArrayList` setup is the noticeable performance win over the default constructor?