Java ArrayList
An ArrayList is Java’s resizable array implementation of the List interface, found in the java.util package. Unlike a plain array, whose length is fixed the moment it’s created, an ArrayList can grow and shrink automatically as you add or remove elements. It’s the collection most Java developers reach for first because it combines fast, indexed access with the convenience of not having to manage sizing yourself.
Overview / How it works
Internally, an ArrayList is backed by a plain Object[] array. When you call add(), the element is placed into that backing array at the next free slot. As long as there’s room, this is a very fast, constant-time operation. The magic happens when the backing array fills up: the ArrayList allocates a brand new, larger array (roughly 1.5 times the old capacity), copies every existing element into it using System.arraycopy, and then continues adding to the new array. This resizing is called growth, and although it’s an O(n) operation when it happens, it happens rarely enough that adding elements is considered amortized O(1).
Because ArrayList is generic (ArrayList<E>), it can only store objects, not primitives. If you write ArrayList<int> it will not compile — you must use the wrapper class instead, e.g. ArrayList<Integer>. Java automatically converts between int and Integer for you (a process called autoboxing and unboxing), but this convenience has a real performance and correctness cost that we’ll cover in the Common Mistakes section.
ArrayList maintains insertion order (elements stay in the order you added them, unless you sort or reorder them), allows duplicate values, and allows null elements. It implements List<E>, which itself extends Collection<E> and Iterable<E>, so it works with the enhanced for-loop, streams, and every general-purpose collection algorithm in java.util.Collections.
Syntax
The general form of declaring and creating an ArrayList looks like this:
ArrayList<Type> listName = new ArrayList<>(); // empty list
ArrayList<Type> listName = new ArrayList<>(initialCapacity); // pre-sized backing array
ArrayList<Type> listName = new ArrayList<>(otherCollection); // copy of another collection
- Type — the element type, which must be a reference/wrapper type (e.g.
String,Integer, a custom class), never a primitive. - new ArrayList<>() — the empty diamond operator
<>lets the compiler infer the type from the left-hand side (available since Java 7). - initialCapacity — an optional hint for how large the backing array should start; it does not limit how many elements you can add, it just avoids early resizing.
The most commonly used methods are summarized below:
| Method | Description |
|---|---|
add(E e) |
Appends an element to the end of the list. |
add(int index, E e) |
Inserts an element at a specific position, shifting later elements right. |
get(int index) |
Returns the element at the given index in O(1) time. |
set(int index, E e) |
Replaces the element at the given index and returns the old value. |
remove(int index) |
Removes by position, shifting later elements left. |
remove(Object o) |
Removes the first occurrence equal to o. |
size() |
Returns the number of elements currently stored. |
contains(Object o) |
Returns true if the list has an element equal to o. |
indexOf(Object o) |
Returns the first index of o, or -1 if not found. |
clear() |
Removes all elements. |
Examples
Example 1: Basic creation and access
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
System.out.println("Fruits: " + fruits);
System.out.println("First fruit: " + fruits.get(0));
System.out.println("Number of fruits: " + fruits.size());
}
}
Output:
Fruits: [Apple, Banana, Cherry]
First fruit: Apple
Number of fruits: 3
Printing an ArrayList directly calls its inherited toString(), which prints all elements comma-separated inside square brackets. get(0) retrieves the element at index 0 in constant time because it’s just an array read under the hood.
Example 2: Removing by value and computing an average
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> scores = new ArrayList<>();
scores.add(85);
scores.add(92);
scores.add(78);
scores.add(60);
scores.remove(Integer.valueOf(60));
int total = 0;
for (int score : scores) {
total += score;
}
double average = (double) total / scores.size();
System.out.println("Scores after removal: " + scores);
System.out.println("Average: " + average);
}
}
Output:
Scores after removal: [85, 92, 78]
Average: 85.0
This example mixes autoboxed Integer values with a plain int accumulator: the enhanced for-loop automatically unboxes each Integer back to an int. Notice Integer.valueOf(60) is used to remove by value rather than by index — more on why that matters shortly.
Example 3: Sorting with Comparator
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Charlie");
names.add("alice");
names.add("Bob");
Collections.sort(names, String.CASE_INSENSITIVE_ORDER);
System.out.println("Sorted (case-insensitive): " + names);
Collections.sort(names, Comparator.reverseOrder());
System.out.println("Reverse sorted: " + names);
}
}
Output:
Sorted (case-insensitive): [alice, Bob, Charlie]
Reverse sorted: [alice, Charlie, Bob]
Collections.sort() accepts a Comparator that controls ordering. String.CASE_INSENSITIVE_ORDER ignores letter case, giving alphabetical order regardless of capitalization. Comparator.reverseOrder() instead uses each string’s natural ordering (plain Unicode character comparison, where uppercase letters sort before lowercase) and reverses it — which is why the second sort doesn’t simply flip the first result.
Under the hood: how resizing actually happens
Walk through what happens when you repeatedly call add() on a fresh ArrayList<Integer>():
- An empty
ArrayListcreated withnew ArrayList<>()doesn’t allocate a 10-element array immediately — it starts with a shared empty array and only allocates a real backing array of capacity 10 on the firstadd()call. - Elements 1 through 10 fill that backing array directly — each
add()is a simple array write plus a size increment. - On the 11th
add(), the list detects the array is full. It computes a new capacity ofoldCapacity + (oldCapacity >> 1), i.e. 10 + 5 = 15, allocates a new 15-slot array, and copies all 10 existing elements into it withSystem.arraycopybefore appending the 11th element. add(int index, E e)andremove(int index)are more expensive: they must shift every element after the target index one slot right (for insert) or left (for remove) usingSystem.arraycopy, making them O(n) in the worst case — inserting or removing near the front of a large list is much slower than doing so at the end.get(index)andset(index, e)are always O(1) because they compute the array offset directly, which is the main advantageArrayListhas over a linked structure.
Common Mistakes
Mistake 1: Modifying a list while iterating with a for-each loop
Removing an element directly inside an enhanced for-loop throws a ConcurrentModificationException, because the loop’s internal iterator detects that the list changed underneath it:
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> items = new ArrayList<>();
items.add("a");
items.add("b");
items.add("c");
for (String item : items) {
if (item.equals("b")) {
items.remove(item); // throws ConcurrentModificationException
}
}
}
}
The fix is to use the iterator’s own remove() method, or the list’s removeIf() method, both of which safely update internal bookkeeping:
import java.util.ArrayList;
import java.util.Iterator;
public class Main {
public static void main(String[] args) {
ArrayList<String> items = new ArrayList<>();
items.add("a");
items.add("b");
items.add("c");
Iterator<String> it = items.iterator();
while (it.hasNext()) {
String item = it.next();
if (item.equals("b")) {
it.remove();
}
}
System.out.println(items);
}
}
Output:
[a, c]
Mistake 2: Confusing remove(int) with remove(Object)
ArrayList is overloaded with two very different remove methods. This trips up almost every beginner working with ArrayList<Integer>:
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.remove(1); // removes the element AT INDEX 1 (the value 20), not the value 1!
System.out.println(numbers);
}
}
Output:
[10, 30]
Because 1 is an int literal, Java resolves the call to remove(int index), which removes by position. To remove the value 1 (or any other boxed value) instead, force the compiler to pick the remove(Object) overload by wrapping the argument with Integer.valueOf(...):
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.remove(Integer.valueOf(1)); // removes the value 1 if present (none here)
System.out.println("After remove(Integer.valueOf(1)): " + numbers);
numbers.remove(Integer.valueOf(20)); // removes the value 20
System.out.println("After remove(Integer.valueOf(20)): " + numbers);
}
}
Output:
After remove(Integer.valueOf(1)): [10, 20, 30]
After remove(Integer.valueOf(20)): [10, 30]
Best Practices
- Declare variables using the
Listinterface type (List<String> names = new ArrayList<>();) so the implementation can be swapped later without touching calling code. - If you know roughly how many elements you’ll store, pass an initial capacity to the constructor to avoid repeated resizing during heavy inserts.
- Prefer
removeIf(Predicate)or an explicitIteratorover removing elements inside an enhanced for-loop. - Remember the
remove(int)vsremove(Object)overload trap whenever the element type is a wrapper likeIntegerorLong. - Avoid frequent insertions or removals near the front of a very large
ArrayList; considerLinkedListorArrayDequeif that access pattern dominates. - An
ArrayListis not thread-safe; useCollections.synchronizedList(...)orCopyOnWriteArrayListwhen multiple threads mutate the same list concurrently. - Use
Collections.sort()orlist.sort(comparator)with aComparatorinstead of writing manual sorting loops.
Practice Exercises
- Create an
ArrayList<String>of five city names, then write code that prints only the cities whose name has more than 5 letters. - Given an
ArrayList<Integer>containing duplicate values, write a program that removes all duplicates while preserving the original order of first appearance. - Write a program that fills an
ArrayList<Integer>with the numbers 1 through 20, then usesremoveIf()to keep only the even numbers, and prints the result.
Summary
ArrayListis a resizable, array-backed implementation of theListinterface injava.util.- It grows automatically by roughly 1.5x when its backing array fills up, copying existing elements into the new array.
get/setby index are O(1); inserting or removing in the middle or at the front is O(n) due to element shifting.- Only reference types (including wrapper classes like
Integer) can be stored — primitives are autoboxed automatically. - Never remove elements from a list inside a plain enhanced for-loop — use an
IteratororremoveIf()instead. - Watch for the
remove(int)vsremove(Object)overload ambiguity withArrayList<Integer>. ArrayListis not synchronized; wrap it if you need thread safety.
