ArrayList in Java – Internals, Performance, and Best Practices

Illustration for ArrayList in Java – Internals, Performance, and Best Practices
By Last updated:

ArrayList is one of the most frequently used classes in the Java Collections Framework. It is a resizable array implementation of the List interface that supports dynamic growth, random access, and predictable performance characteristics. In real-world Java development, ArrayList is the default go-to for ordered, index-based collections.


🔍 What is an ArrayList?

An ArrayList is a dynamic array that automatically resizes itself when elements are added or removed. It allows duplicate elements and maintains the insertion order.

Key Characteristics:

  • Ordered by index (like arrays)
  • Allows duplicates and null values
  • Resizable at runtime
  • Backed by an internal array (Object[])
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
System.out.println(names.get(1)); // Bob

🧠 How ArrayList Works Internally

Internal Structure

ArrayList internally uses an Object[] array. Here’s what happens under the hood:

  • Default initial capacity is 10 (Java 8+)
  • When capacity is exceeded, it resizes to 1.5x the previous size using Arrays.copyOf()
transient Object[] elementData;
private static final int DEFAULT_CAPACITY = 10;

Resizing Logic

newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5x growth

Performance Trade-Off

  • Resizing is costly as it involves array copy.
  • Hence, prefer using ensureCapacity() when size is predictable.

⏱️ Performance Characteristics

Operation Time Complexity
add(E e) O(1) amortized
get(int index) O(1)
remove(int index) O(n)
contains(Object o) O(n)
add(index, E e) O(n)

✅ Common Use Cases

  • Dynamic lists of user input or API responses
  • Maintaining order while allowing duplicates
  • Lists that grow and shrink frequently

Example:

List<Integer> scores = new ArrayList<>();
Collections.addAll(scores, 95, 88, 76, 88);

🧪 Practical Example with Streams

List<String> words = Arrays.asList("java", "spring", "boot");
List<String> upper = words.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());

System.out.println(upper); // [JAVA, SPRING, BOOT]

🔁 ArrayList vs LinkedList vs Vector

Feature ArrayList LinkedList Vector
Resize Cost Medium None Medium
Access Fast (O(1)) Slow (O(n)) Fast (O(1))
Thread-Safety No No Yes (synchronized)
Memory Overhead Low High Low

🚨 Common Pitfalls and Anti-patterns

❌ Adding while iterating (without iterator)

for (String s : list) {
    list.remove(s); // Throws ConcurrentModificationException
}

✅ Fix:

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    it.next();
    it.remove();
}

✨ Best Practices

  • Use initialCapacity if size is known
  • Avoid frequent remove() from large lists
  • Use Collections.unmodifiableList() when immutability is required
  • Prefer List.of() for fixed-size unmodifiable lists (Java 9+)
List<String> fixed = List.of("A", "B", "C"); // Java 9

📌 What's New in Java Versions?

Java 8

  • Introduced Streams API for functional programming
  • Useful methods: forEach(), removeIf(), replaceAll()

Java 9

  • List.of(...) for immutable lists

Java 10

  • var for local variable type inference

Java 11

  • Minor performance improvements

Java 17

  • Stable LTS for sealed classes, better GC

Java 21

  • Further performance boosts, preview structured concurrency

🔄 Refactoring Legacy Code

Before:

List names = new ArrayList();
names.add("John");
String name = (String) names.get(0);

After (Java 8+):

List<String> names = new ArrayList<>();
names.add("John");
String name = names.get(0);

🧠 Real-World Analogy

Think of an ArrayList like a train with fixed compartments (array). When full, a new train with more compartments is created, and passengers are moved over. That "moving" is resizing.


❓ FAQ: Expert-Level Insights

  1. What is the default capacity of an ArrayList?

    • 10 (Java 8+)
  2. Is ArrayList synchronized?

    • No. Use Collections.synchronizedList() or CopyOnWriteArrayList.
  3. Can we store nulls?

    • Yes, multiple null values are allowed.
  4. Why is remove(index) O(n)?

    • Because it shifts all elements to the left after removal.
  5. How to clone an ArrayList?

    List<String> clone = new ArrayList<>(original);
    
  6. What happens if capacity is exceeded?

    • It resizes the array to 1.5x of current capacity.
  7. Which is better: ArrayList or LinkedList?

    • ArrayList for access-heavy operations; LinkedList for frequent insertions/removals.
  8. How to make ArrayList immutable?

    • Collections.unmodifiableList(list) or List.of(...)
  9. How to avoid ConcurrentModificationException?

    • Use Iterator or CopyOnWriteArrayList
  10. Does ArrayList shrink automatically?

    • No. Call trimToSize() manually.

🏁 Conclusion and Key Takeaways

  • Use ArrayList when you need fast access, predictable order, and dynamic resizing.
  • Avoid it for frequent insertions/removals at the start/middle.
  • Combine with Java 8+ features to make your code concise and expressive.
  • Understand internals to write performant, maintainable code.

Pro Tip: For small collections, List.of() (Java 9+) is your best friend — clean, immutable, and concise.