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 ArrayList created with new 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 first add() 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 of oldCapacity + (oldCapacity >> 1), i.e. 10 + 5 = 15, allocates a new 15-slot array, and copies all 10 existing elements into it with System.arraycopy before appending the 11th element.
  • add(int index, E e) and remove(int index) are more expensive: they must shift every element after the target index one slot right (for insert) or left (for remove) using System.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) and set(index, e) are always O(1) because they compute the array offset directly, which is the main advantage ArrayList has 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 List interface 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 explicit Iterator over removing elements inside an enhanced for-loop.
  • Remember the remove(int) vs remove(Object) overload trap whenever the element type is a wrapper like Integer or Long.
  • Avoid frequent insertions or removals near the front of a very large ArrayList; consider LinkedList or ArrayDeque if that access pattern dominates.
  • An ArrayList is not thread-safe; use Collections.synchronizedList(...) or CopyOnWriteArrayList when multiple threads mutate the same list concurrently.
  • Use Collections.sort() or list.sort(comparator) with a Comparator instead 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 uses removeIf() to keep only the even numbers, and prints the result.

Summary

  • ArrayList is a resizable, array-backed implementation of the List interface in java.util.
  • It grows automatically by roughly 1.5x when its backing array fills up, copying existing elements into the new array.
  • get/set by 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 Iterator or removeIf() instead.
  • Watch for the remove(int) vs remove(Object) overload ambiguity with ArrayList<Integer>.
  • ArrayList is not synchronized; wrap it if you need thread safety.