ArrayList
When Java developers need a general-purpose List for storing an ordered group of elements, ArrayList is very often the first implementation they reach for. It is simple, flexible, fast for positional access, and widely used in application code.
ArrayList is an implementation of the List interface backed by a resizable array. Unlike a traditional fixed-size Java array, an ArrayList can automatically grow and shrink as elements are added or removed.
Core Idea: ArrayList gives you the convenience of a dynamically sized array while providing the rich operations defined by the List interface.
Why Does ArrayList Exist?
A normal Java array has a fixed length. Once you create an array, its size cannot be changed.
String[] languages = new String[3]; languages[0] = "Java"; languages[1] = "C#"; languages[2] = "Python"; // The array cannot automatically grow here.
What if you later need to store a fourth language? You would have to create a larger array and manually copy the existing elements.
ArrayList handles this resizing process for you.
List<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("C#");
languages.add("Python");
languages.add("JavaScript");
System.out.println(languages);
You do not need to know the final number of elements when creating the ArrayList.
Simple analogy: A normal array is like a shelf built with a fixed number of spaces. ArrayList is like an expandable shelf that can make room when more items arrive.
ArrayList Hierarchy
ArrayList implements the List interface and therefore also inherits the general collection behavior defined higher in the hierarchy.
Collection
|
+-- List
|
+-- ArrayList
ArrayList is a concrete class, which means it can be instantiated using the new keyword.
Creating an ArrayList
The recommended approach is usually to declare the reference using the List interface.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Bibhu");
names.add("Rahul");
names.add("Priya");
System.out.println(names);
}
}
The diamond operator <> allows Java to infer the generic type from the variable declaration.
Preferred style: List<String> names = new ArrayList<>(); keeps your code dependent on the List abstraction rather than unnecessarily exposing the concrete implementation.
Adding Elements
The add() method appends an element to the end of the ArrayList.
List<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("C#");
languages.add("Python");
System.out.println(languages);
ArrayList maintains the order in which elements are inserted.
Adding an Element at a Specific Position
You can insert an element at a specific index using the overloaded add(index, element) method.
List<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("Python");
// Insert at index 1.
languages.add(1, "C#");
System.out.println(languages);
The existing element at that position and subsequent elements are shifted to the right.
Performance Insight: Inserting near the beginning or middle of an ArrayList can require shifting many elements. Appending at the end is typically much cheaper.
Accessing Elements
ArrayList provides fast index-based access through the get() method.
List<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("C#");
languages.add("Python");
String language = languages.get(1);
System.out.println(language);
Because ArrayList is backed by an array-like structure, retrieving an element by index is typically an O(1) operation.
Updating Elements
The set() method replaces the element at a specified index.
List<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("C#");
languages.add("Python");
languages.set(1, "JavaScript");
System.out.println(languages);
The List size does not change because an existing element is replaced.
Removing Elements
ArrayList supports removing elements by index or by object value.
List<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("C#");
languages.add("Python");
// Remove by index.
languages.remove(1);
// Remove by value.
languages.remove("Python");
System.out.println(languages);
When working with List<Integer>, remember the overloaded remove() behavior discussed earlier: an int argument selects the index-based overload.
Integer remove() Example
List<Integer> numbers = new ArrayList<>(); numbers.add(10); numbers.add(20); numbers.add(30); // Removes the element at index 1. numbers.remove(1); System.out.println(numbers);
To remove the value 20 instead of the element at index 20, use an Integer object:
numbers.remove(Integer.valueOf(20));
Interview Favorite: The difference between remove(int) and remove(Object) is a classic Java question because of method overloading and autoboxing.
Checking Size and Capacity
The size() method tells you how many elements are currently stored.
List<String> names = new ArrayList<>();
names.add("Bibhu");
names.add("Rahul");
System.out.println(names.size());
Do not confuse size with the ArrayList's internal capacity. Size is the number of actual elements. Capacity refers to how many elements the internal storage can accommodate before another resize is required.
ArrayList Capacity
ArrayList internally manages a resizable array. When the current storage becomes insufficient, it allocates a larger array and moves the existing elements into the new storage.
List<String> names = new ArrayList<>();
names.add("A");
names.add("B");
names.add("C");
The exact growth policy is an implementation detail and should not be hard-coded into application logic. The important concept is that ArrayList grows automatically when necessary.
Performance Insight: Resizing is not performed for every individual insertion. ArrayList grows its internal storage in larger steps, which makes repeated append operations efficient on an amortized basis.
Specifying Initial Capacity
If you have a reasonable estimate of how many elements will be stored, you can provide an initial capacity.
List<String> users = new ArrayList<>(1000);
This does not add 1000 elements. It gives the ArrayList an initial storage capacity for approximately that many elements.
Important: new ArrayList<>(1000) does not mean size 1000. The List is still empty; the number represents initial capacity.
Checking Whether ArrayList Is Empty
List<String> names = new ArrayList<>();
if (names.isEmpty()) {
System.out.println("No names available");
}
Use isEmpty() when your intention is simply to determine whether the List contains no elements.
Checking for an Element
The contains() method checks whether a matching element exists.
List<String> skills = new ArrayList<>();
skills.add("Java");
skills.add("Spring");
skills.add("SQL");
if (skills.contains("Java")) {
System.out.println("Java is available");
}
The comparison relies on the equality behavior of the stored objects.
Finding an Element's Position
List<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("C#");
languages.add("Java");
System.out.println(languages.indexOf("Java"));
System.out.println(languages.lastIndexOf("Java"));
indexOf() returns the first matching index, while lastIndexOf() returns the last matching index.
Iterating Through ArrayList
The enhanced for loop is often the cleanest way to process every element.
List<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("C#");
languages.add("Python");
for (String language : languages) {
System.out.println(language);
}
When the index is important, use an indexed loop.
for (int i = 0; i < languages.size(); i++) {
System.out.println(
i + " : " + languages.get(i)
);
}
Using Iterator
ArrayList can also be traversed using an Iterator.
Iterator<String> iterator = languages.iterator();
while (iterator.hasNext()) {
String language = iterator.next();
System.out.println(language);
}
An Iterator is particularly useful when you need to safely remove elements during iteration using the Iterator's own remove() method.
Removing While Iterating
A common mistake is modifying an ArrayList structurally inside an enhanced for loop.
List<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("C#");
languages.add("Python");
// Avoid directly modifying the List
// during an enhanced for loop.
// This can cause ConcurrentModificationException.
If removal is required during iteration, an Iterator can be used:
Iterator<String> iterator = languages.iterator();
while (iterator.hasNext()) {
String language = iterator.next();
if (language.equals("C#")) {
iterator.remove();
}
}
Practical Rule: If you structurally modify an ArrayList while iterating, use an appropriate removal mechanism such as Iterator.remove(), removeIf(), or another approach that matches your requirement.
Removing with removeIf()
Modern Java provides removeIf(), which is often cleaner when the removal condition can be expressed as a predicate.
List<Integer> numbers = new ArrayList<>(); numbers.add(10); numbers.add(15); numbers.add(20); numbers.add(25); numbers.removeIf(number -> number % 2 != 0); System.out.println(numbers);
The example removes all odd numbers from the ArrayList.
Sorting an ArrayList
ArrayList inherits List's sorting capabilities.
List<Integer> numbers = new ArrayList<>(); numbers.add(40); numbers.add(10); numbers.add(30); numbers.add(20); numbers.sort(null); System.out.println(numbers);
The List is sorted according to the elements' natural ordering.
For custom ordering, provide a Comparator.
numbers.sort((a, b) -> b - a); System.out.println(numbers);
Converting ArrayList to Array
Sometimes an API expects an array instead of a List. ArrayList can be converted using toArray().
List<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("C#");
languages.add("Python");
String[] array = languages.toArray(new String[0]);
System.out.println(array[0]);
The resulting array is separate from the ArrayList's internal storage.
Copying an ArrayList
You can create a new ArrayList from an existing collection.
List<String> original = new ArrayList<>();
original.add("Java");
original.add("Spring");
List<String> copy = new ArrayList<>(original);
System.out.println(copy);
This creates a new List structure containing references to the same element objects. It is therefore a shallow copy, not a deep copy of the objects themselves.
ArrayList and Duplicate Elements
ArrayList follows the List contract, so duplicate elements are allowed.
List<String> skills = new ArrayList<>();
skills.add("Java");
skills.add("Spring");
skills.add("Java");
System.out.println(skills);
Both occurrences of Java remain because each occupies its own position.
ArrayList and Null
ArrayList permits null elements.
List<String> values = new ArrayList<>();
values.add("Java");
values.add(null);
values.add("Spring");
System.out.println(values);
Although null is permitted, using null extensively can make application logic harder to reason about. Use it deliberately rather than simply because the collection permits it.
ArrayList and Generics
Generics provide compile-time type safety and are strongly recommended.
List<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("Spring");
// Compile-time error:
// languages.add(100);
Without generics, raw collections can contain unrelated object types and may require unsafe casts later.
ArrayList Performance
Understanding ArrayList's performance helps you choose it intelligently rather than simply using it because it is familiar.
| Operation | Typical Complexity | Reason |
|---|---|---|
| get(index) | O(1) | Direct positional access through the underlying array. |
| set(index, value) | O(1) | Direct replacement at an index. |
| add(value) at end | O(1) amortized | Usually appends to available capacity; occasional resize costs more. |
| add(index, value) | O(n) | Elements may need to be shifted. |
| remove(index) | O(n) | Elements after the removed position may need to shift. |
| contains(value) | O(n) | May require scanning elements. |
| indexOf(value) | O(n) | Searches through the List. |
These are typical complexity characteristics, not guarantees that every operation will take exactly the same amount of time in every situation.
Why ArrayList Is Often the Default List
ArrayList is frequently preferred because many real-world applications perform a combination of operations where fast positional access and efficient sequential traversal are valuable.
- Fast index-based access.
- Efficient append operations on an amortized basis.
- Compact array-based storage compared with linked nodes.
- Good cache locality for sequential access.
- Simple and familiar API.
- Works naturally with the List abstraction.
Industry Insight: The default choice should still be driven by access patterns. "Use ArrayList everywhere" is not a design principle; "use ArrayList when its characteristics fit the workload" is.
ArrayList vs LinkedList
ArrayList and LinkedList both implement List, but their internal structures are very different.
| Feature | ArrayList | LinkedList |
|---|---|---|
| Internal structure | Resizable array | Doubly linked nodes |
| get(index) | Typically O(1) | Typically O(n) |
| Append at end | O(1) amortized | Typically O(1) |
| Insert/remove in middle | Typically O(n) due to shifting | O(1) once the node position is reached |
| Memory overhead | Generally lower | Generally higher |
| Cache locality | Generally better | Generally worse |
| Typical default | Yes | Only when specific characteristics justify it |
Notice the important qualification for LinkedList: saying "insertion is O(1)" without considering how you locate the insertion point is incomplete. If you first need to traverse the list to find that position, the overall operation can still be O(n).
ArrayList and Thread Safety
ArrayList is not synchronized. Multiple threads modifying the same ArrayList concurrently require appropriate synchronization or a suitable concurrent collection strategy.
List<String> names = new ArrayList<>(); // ArrayList itself does not make concurrent // modifications thread-safe.
If your application requires synchronized access, choose an appropriate concurrency design rather than assuming ArrayList automatically handles multiple threads safely.
Unmodifiable ArrayList-Like Lists
If you need a List that callers should not structurally modify, modern Java provides factory methods such as List.of().
List<String> languages = List.of(
"Java",
"Spring",
"SQL"
);
System.out.println(languages);
This is not an ArrayList. It is an unmodifiable List returned by the Java API. The important lesson is that the List interface can have many implementations with different characteristics.
Common Beginner Mistakes
- Confusing size with capacity: An ArrayList can have unused internal capacity beyond its current number of elements.
- Assuming add(index, value) is O(1): Inserting into the middle can require shifting elements.
- Assuming LinkedList is always faster for insertion: The cost of reaching the insertion position must also be considered.
- Using raw ArrayList: Prefer generics such as ArrayList<String>.
- Removing directly during enhanced for iteration: This can cause ConcurrentModificationException.
- Forgetting remove(int) behavior: With List<Integer>, an int argument selects index-based removal.
- Assuming ArrayList is thread-safe: It is not inherently synchronized.
- Choosing ArrayList without considering requirements: Collection selection should follow the application's access pattern.
Best Practices
- Prefer List<T> as the reference type when implementation-specific behavior is unnecessary.
- Use ArrayList as a strong general-purpose List default.
- Provide an initial capacity when you have a reliable estimate of the collection's size and want to reduce resizing overhead.
- Use ArrayList when frequent index-based access and sequential traversal are important.
- Use Iterator.remove(), removeIf(), or another appropriate technique when removing elements during iteration.
- Use generics to maintain compile-time type safety.
- Do not rely on ArrayList for thread safety; choose an appropriate concurrency strategy when needed.
Interview Insights
Question: What is ArrayList?
Answer: ArrayList is a resizable-array implementation of the List interface. It maintains insertion order, permits duplicate elements, and provides efficient index-based access.
Question: Why is get(index) fast in ArrayList?
Answer: ArrayList stores elements in an array-like structure, allowing direct positional access without traversing earlier elements.
Question: What happens internally when ArrayList runs out of capacity?
Answer: It allocates a larger internal array and copies the existing elements into the new storage. The exact growth policy is an implementation detail.
Question: Is ArrayList thread-safe?
Answer: No. ArrayList does not provide built-in synchronization for concurrent access.
Question: What is the difference between size and capacity in ArrayList?
Answer: Size is the number of elements currently stored. Capacity is the amount of internal storage available before another resize is required.
Quick Revision
| Concept | Key Point |
|---|---|
| ArrayList | Resizable-array implementation of List. |
| Order | Maintains element order. |
| Duplicates | Allowed. |
| get(index) | Typically O(1) positional access. |
| add(element) | Appends to the end; typically O(1) amortized. |
| add(index, element) | May require shifting elements and is typically O(n). |
| remove(index) | May require shifting elements and is typically O(n). |
| contains() | Typically O(n) because it may scan the List. |
| Initial capacity | Controls initial internal storage; it does not determine List size. |
| Thread safety | ArrayList is not inherently synchronized. |
| Best general use | Ordered data requiring frequent access by index and efficient sequential traversal. |
ArrayList is popular for a very good reason: it provides a practical balance of simplicity, flexibility, memory efficiency, and fast positional access for a wide range of applications. The most important lesson is not simply to memorize that ArrayList is "fast." Understand why it is fast, where shifting occurs, how resizing works, and when another data structure would better match the workload. In the next chapter, we will examine LinkedList and compare its linked-node structure with ArrayList in greater depth.
