ArrayList

ArrayList internals, performance characteristics, and the patterns that make it the most-used collection in Java.

Last reviewed: January 2025

·Java 8Java 11Java 17Java 21
✅
Quick Understanding: ArrayList is a resizable array. It is the go-to List implementation for 95% of use cases because it offers O(1) random access and excellent cache performance. The only time you need something else is when you frequently insert/delete in the middle of large lists, or need thread safety.

🔍 What Is It?

ArrayList is java.util.List backed by a dynamic array (Object[]).

Key Characteristics:

  • →Random access: O(1) — direct array index
  • →Add at end: O(1) amortized — occasionally O(n) for resize
  • →Add/remove in middle: O(n) — must shift elements
  • →Search (contains): O(n) — must scan entire array
  • →Memory: contiguous memory — excellent CPU cache performance

Capacity vs Size:

  • →size() — number of elements currently stored
  • →capacity — size of the internal array (always >= size)
  • →Initial capacity: 10 (or specified in constructor)
  • →Growth factor: 1.5x (new capacity = old * 3/2 + 1)

Constructors:

  • →new ArrayList<>() — initial capacity 10
  • →new ArrayList<>(initialCapacity) — pre-allocate
  • →new ArrayList<>(collection) — copy from collection

🎯 Why Do We Need It?

ArrayList is the workhorse of Java collections. It combines the simplicity of arrays (fast random access, contiguous memory) with the flexibility of dynamic sizing. In enterprise code, you use it to hold query results, build response objects, accumulate items, and pass data between layers.

🏗️ What Happens in Real Projects?

ArrayList is used in every Java project:

  • →Query results: List<Order> orders = new ArrayList<>(orderRepository.findAll())
  • →Building responses: accumulate items then return
  • →Batch processing: collect items then process in bulk
  • →Sorting: ArrayList.sort() is highly optimized (TimSort)

⚙️ How It Works

How Resizing Works:

Initial: [_, _, _, _, _, _, _, _, _, _]  // capacity 10, size 0
After 10 adds: [a, b, c, d, e, f, g, h, i, j]  // capacity 10, size 10
11th add triggers resize:
  1. Create new array of size 15 (10 * 3/2 + 1)
  2. Copy all 10 elements to new array
  3. Add 11th element
  4. Old array becomes garbage

Why O(1) amortized for add:

Each element is copied at most once per resize. Total copies for n elements = n/2 + n/4 + ... = n. So average copies per element = 1. Hence O(1) amortized.

trimToSize():

Reduces internal array to exactly size(). Use after bulk loading to free unused capacity.

💻 Example

ArrayList Best Practices — How to use ArrayList efficiently in production code.

java
// Pre-size when you know the approximate count
// Avoids multiple resize operations
public List<ProductDto> convertToDto(List<Product> products) {
    List<ProductDto> dtos = new ArrayList<>(products.size());  // exact size
    for (Product p : products) {
        dtos.add(new ProductDto(p.getId(), p.getName(), p.getPrice()));
    }
    return dtos;
}

// Efficient bulk add
List<String> result = new ArrayList<>();
result.addAll(firstBatch);   // more efficient than adding one by one
result.addAll(secondBatch);

// removeIf — efficient in-place removal (avoids ConcurrentModificationException)
List<Order> orders = new ArrayList<>(fetchOrders());
orders.removeIf(order -> order.getStatus() == OrderStatus.CANCELLED);

// replaceAll — in-place transformation
List<String> names = new ArrayList<>(List.of("alice", "bob", "charlie"));
names.replaceAll(String::toUpperCase);
// [ALICE, BOB, CHARLIE]

// Sort with Comparator
List<Employee> employees = getEmployees();
employees.sort(Comparator
    .comparing(Employee::getDepartment)
    .thenComparing(Employee::getName));

// Efficient contains check for large lists — use Set instead
List<String> bigList = new ArrayList<>(100_000);
// BAD: O(n) per check
boolean found = bigList.contains("target");
// GOOD: convert to Set first if doing many contains checks
Set<String> lookupSet = new HashSet<>(bigList);
boolean fastFound = lookupSet.contains("target");  // O(1)

🤔 Common Confusions

❓ Why is ArrayList faster than LinkedList for most operations?

CPU cache locality. ArrayList stores elements in contiguous memory — when you access one element, nearby elements are loaded into CPU cache. Subsequent accesses are cache hits (very fast). LinkedList nodes are scattered across the heap — each access is likely a cache miss. This hardware effect makes ArrayList faster even for operations where LinkedList should theoretically be equal or better.

❓ What is the difference between ArrayList and Vector?

Vector is the legacy synchronized ArrayList from Java 1.0. Every method is synchronized, making it thread-safe but slow. ArrayList is not synchronized and faster for single-threaded use. For thread-safe lists, use CopyOnWriteArrayList or Collections.synchronizedList(). Never use Vector in new code.

🚨 Common Production Issues

Storing large objects in ArrayList — holding references prevents GC. If you load 100,000 JPA entities into a List, they all stay in memory until the list is cleared.

ArrayList.subList() is a view — modifying the parent list while holding a subList reference throws ConcurrentModificationException.

Autoboxing overhead — List<Integer> boxes every int. For large lists of primitives, use int[] or IntStream.

✅ Best Practices

Pre-size ArrayList when you know the approximate size: new ArrayList<>(expectedSize).

Use removeIf() for in-place filtering instead of iterating and removing.

Use replaceAll() for in-place transformation.

For frequent contains() checks on large lists, use a HashSet instead.

Return List<T> (interface), not ArrayList<T> (implementation).

🏛️ Architect's Note

Senior Architect's Perspective

For very large lists (millions of elements), consider alternatives: arrays (int[], Object[]) have less overhead; Stream<T> enables lazy processing without materializing the full list; database pagination avoids loading all records. The right tool depends on whether you need random access, streaming, or just iteration.

💬 Frequently Asked Questions

How do I safely iterate and remove elements from an ArrayList?

Three options: (1) Use removeIf() — cleanest: list.removeIf(item -> shouldRemove(item)); (2) Use Iterator explicitly: Iterator<T> it = list.iterator(); while (it.hasNext()) { if (shouldRemove(it.next())) it.remove(); }; (3) Iterate backwards: for (int i = list.size()-1; i >= 0; i--) { if (shouldRemove(list.get(i))) list.remove(i); }. Never use for-each and call list.remove() — that throws ConcurrentModificationException.

🎯 Key Takeaways

1ArrayList is backed by a dynamic array — O(1) random access, O(n) insert/delete in middle
2Pre-size with new ArrayList<>(expectedSize) to avoid resize operations
3Use removeIf() and replaceAll() for in-place modification
4Not thread-safe — use CopyOnWriteArrayList or synchronizedList for concurrent access
5For frequent contains() on large lists, use HashSet instead
6Return List<T> interface, not ArrayList<T> concrete type

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these Core Java references to verify the concept in official docs and continue into the most relevant SplashRide pages.