Introduction to ArrayList
ArrayList is one of the most frequently used classes in the Java Collections Framework. It provides a resizable array implementation of the
List
interface, which means it can grow and shrink dynamically as you add or remove elements. Unlike a standard Java array, whose size is fixed at the time of creation, an ArrayList handles resizing automatically behind the scenes.
The class belongs to the
java.util
package and has been part of Java since version 1.2. In real-world applications, you will encounter ArrayList almost everywhere — from storing query results to holding lists of domain objects in a Spring Boot controller. Understanding how it works internally and knowing its strengths and limitations will help you write more efficient and maintainable Java code.
When to Use ArrayList
Choose ArrayList when you need a collection that grows dynamically, when you frequently access elements by their index, and when you mostly add elements at the end of the list. If your application performs many insertions or deletions in the middle of a large list, consider LinkedList instead. For unique elements with fast lookups, look into HashSet .
Prerequisites
Before working through this tutorial, you should be comfortable with the following Java fundamentals:
- Basic Java syntax, including variable declarations and data types
- How to create and use standard arrays
- Classes and objects , including constructors and method calls
- Basic understanding of generics (the angle-bracket notation for type parameters)
-
The concept of interfaces (ArrayList implements the
Listinterface)
If any of these topics feel unfamiliar, we recommend completing the linked tutorials first. Having a solid foundation will make the ArrayList concepts much easier to grasp.
What You Will Learn
By the end of this tutorial, you will be able to:
- Create and initialize ArrayLists using different approaches
- Add, remove, access, and update elements in an ArrayList
- Iterate over an ArrayList using multiple techniques
- Sort and search elements within an ArrayList
- Explain the internal mechanics of how ArrayList grows
- Identify and avoid common ArrayList pitfalls
- Apply best practices for performance and readability
- Decide when ArrayList is the right choice versus other collection types
How ArrayList Works Internally
At its core, an ArrayList is backed by a regular Java array. When you create an ArrayList with the default constructor, it allocates an empty array with an initial capacity of 10. As you add elements, they are placed into this backing array one by one.
The critical difference from a plain array is what happens when the backing array becomes full. Instead of throwing an error, the ArrayList automatically performs a resize operation :
- A new, larger array is allocated (typically 1.5 times the previous capacity)
- All existing elements are copied from the old array into the new one
- The internal reference is updated to point to the new array
- The old array becomes eligible for garbage collection
This resizing is transparent to you as the developer, but it has a real performance cost — copying n elements takes O(n) time. If you know in advance roughly how many elements the list will hold, you can specify an initial capacity to avoid unnecessary resizes.
ArrayList preserves the order in which elements were added (insertion order), allows duplicate elements, and permits
null
values. It does
not
provide any built-in synchronization, meaning it is not safe for concurrent modification by multiple threads without external synchronization.
Key Characteristics at a Glance
-
Implements the
Listinterface (and thereforeCollectionandIterable) - Allows duplicate and null elements
- Maintains insertion order
- Provides O(1) random access by index
- Amortized O(1) append at the end
- O(n) insertion or deletion in the middle (due to element shifting)
- Not thread-safe
Creating an ArrayList — Syntax
There are several ways to create an ArrayList. The most common forms are shown below:
import java.util.ArrayList;
import java.util.List;
// 1. Empty ArrayList with default initial capacity (10)
List<String> names = new ArrayList<>();
// 2. Empty ArrayList with a specified initial capacity
List<Integer> numbers = new ArrayList<>(500);
// 3. ArrayList initialized from another collection
List<String> original = List.of("A", "B", "C");
List<String> copy = new ArrayList<>(original);
// 4. Using List.of() for an immutable list (Java 9+)
List<String> immutable = List.of("X", "Y", "Z");
Declare as List, Not ArrayList
Notice that the variable type above is
List<String>
, not
ArrayList<String>
. This follows the principle of
programming to an interface
. If you later decide to switch to a
LinkedList
, you only need to change the right-hand side of the assignment — the rest of your code continues to work because it depends on the
List
interface, not a specific implementation.
Simple Example — Adding and Accessing Elements
Here is a straightforward example that creates an ArrayList of strings, adds a few elements, and then accesses them:
import java.util.ArrayList;
import java.util.List;
public class ArrayListBasicExample {
public static void main(String[] args) {
// Create an ArrayList to hold fruit names
List<String> fruits = new ArrayList<>();
// Add elements — they are appended to the end
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
fruits.add("Mango");
// Print the entire list
System.out.println("Fruits: " + fruits);
// Access an element by index (zero-based)
String second = fruits.get(1);
System.out.println("Second fruit: " + second);
// Get the number of elements
System.out.println("Total fruits: " + fruits.size());
// Check whether the list contains a specific value
boolean hasMango = fruits.contains("Mango");
System.out.println("Contains Mango? " + hasMango);
}
}
How the Example Works
Let's walk through the code line by line to understand exactly what happens:
-
import java.util.List;— We import theListinterface because we are declaring our variable asList<String>. TheArrayListimport brings in the concrete class. -
List<String> fruits = new ArrayList<>();— This creates an empty ArrayList backed by an array of capacity 10. The diamond operator<>tells the compiler to infer the generic type (String) from the left-hand side. -
fruits.add("Apple");— Theadd()method appends the string to the end of the backing array and increments the internalsizecounter. -
System.out.println("Fruits: " + fruits);— When an ArrayList is concatenated with a string, itstoString()method is called automatically. The default implementation returns the elements enclosed in square brackets, separated by commas. -
fruits.get(1);— This directly accesses position 1 in the backing array, which is the second element ("Banana") because indexing starts at 0. This operation runs in constant time O(1). -
fruits.size();— Returns the current number of elements (4), which is not the same as the backing array's capacity (10). -
fruits.contains("Mango");— Iterates through the list and checks each element usingequals(). Returnstrueas soon as a match is found. This runs in O(n) time in the worst case.
Common ArrayList Operations
ArrayList provides a rich set of methods. Below is a practical example that covers the operations you will use most often:
import java.util.ArrayList;
import java.util.List;
public class ArrayListOperations {
public static void main(String[] args) {
List<String> colors = new ArrayList<>();
// --- Adding elements ---
colors.add("Red"); // appends to the end
colors.add("Green");
colors.add("Blue");
colors.add(1, "Yellow"); // inserts at index 1, shifts later elements right
System.out.println("After adds: " + colors);
// --- Updating an element ---
colors.set(0, "Crimson"); // replaces element at index 0
System.out.println("After set: " + colors);
// --- Removing elements ---
colors.remove(2); // removes by index
colors.remove("Yellow"); // removes by value (first occurrence)
System.out.println("After removes: " + colors);
// --- Checking contents ---
System.out.println("Is empty? " + colors.isEmpty());
System.out.println("Index of Crimson: " + colors.indexOf("Crimson"));
// --- Converting to an array ---
String[] array = colors.toArray(new String[0]);
System.out.println("Array length: " + array.length);
// --- Clearing all elements ---
colors.clear();
System.out.println("After clear: " + colors);
}
}
Key observations from this example:
-
add(index, element)shifts every element from that index onward one position to the right. For a list with n elements, this is an O(n) operation. -
set(index, element)directly replaces the element at the given position — it does not shift anything and runs in O(1). -
remove(int)removes by index and shifts the remaining elements left.remove(Object)removes the first occurrence of the specified value. Both are O(n). -
toArray(new String[0])is the recommended way to convert a List to an array. Passing a zero-length array lets the JVM allocate a correctly sized array internally.
Iterating Over an ArrayList
There are several ways to loop through the elements of an ArrayList. Each approach has its own use case:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ArrayListIteration {
public static void main(String[] args) {
List<String> languages = new ArrayList<>();
languages.add("Java");
languages.add("Python");
languages.add("C++");
languages.add("Go");
// 1. Classic for loop with index
System.out.println("Classic for loop:");
for (int i = 0; i < languages.size(); i++) {
System.out.println((i) + ": " + languages.get(i));
}
// 2. Enhanced for loop (for-each)
System.out.println("\nEnhanced for loop:");
for (String lang : languages) {
System.out.println(lang);
}
// 3. Iterator (safe for removal during iteration)
System.out.println("\nIterator:");
Iterator<String> it = languages.iterator();
while (it.hasNext()) {
String lang = it.next();
System.out.println(lang);
}
// 4. forEach with lambda (Java 8+)
System.out.println("\nforEach with lambda:");
languages.forEach(lang -> System.out.println(lang));
}
}
Which Iteration Style Should You Use?
For most read-only cases, the enhanced for loop or
forEach
with a lambda is the cleanest option. Use the classic for loop only when you need the index. Use an
Iterator
when you need to remove elements during iteration — the for-each loop will throw a
ConcurrentModificationException
if you try to call
remove()
on the list itself while iterating.
Sorting an ArrayList
Java provides convenient methods for sorting ArrayList elements. The approach depends on whether you are sorting primitive wrappers, strings, or custom objects:
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class ArrayListSorting {
public static void main(String[] args) {
// Sorting strings (natural order — alphabetical)
List<String> names = new ArrayList<>();
names.add("Charlie");
names.add("Alice");
names.add("Bob");
names.sort(null); // null means natural ordering for Comparable types
System.out.println("Alphabetical: " + names);
// Sorting in reverse order
names.sort(Comparator.reverseOrder());
System.out.println("Reverse: " + names);
// Sorting integers
List<Integer> numbers = new ArrayList<>();
numbers.add(42);
numbers.add(7);
numbers.add(15);
numbers.add(3);
numbers.sort(Comparator.naturalOrder());
System.out.println("Ascending: " + numbers);
numbers.sort(Comparator.reverseOrder());
System.out.println("Descending: " + numbers);
}
}
For sorting custom objects, your class must implement
Comparable
and override
compareTo()
, or you can provide a standalone
Comparator
. Here is a quick example using a Comparator with a custom class:
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
class Product {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() { return name; }
public double getPrice() { return price; }
@Override
public String toString() {
return name + " ($" + price + ")";
}
}
public class CustomObjectSorting {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Laptop", 999.99));
products.add(new Product("Mouse", 25.50));
products.add(new Product("Keyboard", 79.99));
// Sort by price ascending
products.sort(Comparator.comparingDouble(Product::getPrice));
System.out.println("By price (low to high): " + products);
// Sort by name alphabetically
products.sort(Comparator.comparing(Product::getName));
System.out.println("By name: " + products);
}
}
Real-World Example — Task Manager
Let's build a small task management program that demonstrates how ArrayList works with a custom class. This kind of structure is common in real applications where you need to track a collection of domain objects:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
class Task {
private int id;
private String description;
private boolean completed;
public Task(int id, String description) {
this.id = id;
this.description = description;
this.completed = false;
}
public int getId() { return id; }
public String getDescription() { return description; }
public boolean isCompleted() { return completed; }
public void markComplete() { this.completed = true; }
@Override
public String toString() {
return "[" + id + "] " + description
+ (completed ? " (DONE)" : " (PENDING)");
}
}
public class TaskManager {
private List<Task> tasks;
private int nextId;
public TaskManager() {
tasks = new ArrayList<>();
nextId = 1;
}
public void addTask(String description) {
tasks.add(new Task(nextId++, description));
System.out.println("Added task: " + description);
}
public void completeTask(int id) {
for (Task task : tasks) {
if (task.getId() == id) {
task.markComplete();
System.out.println("Completed: " + task);
return;
}
}
System.out.println("Task not found with ID: " + id);
}
public void removeCompletedTasks() {
Iterator<Task> iterator = tasks.iterator();
int removed = 0;
while (iterator.hasNext()) {
if (iterator.next().isCompleted()) {
iterator.remove();
removed++;
}
}
System.out.println("Removed " + removed + " completed task(s).");
}
public void displayPendingTasks() {
System.out.println("\n--- Pending Tasks ---");
boolean found = false;
for (Task task : tasks) {
if (!task.isCompleted()) {
System.out.println(task);
found = true;
}
}
if (!found) {
System.out.println("No pending tasks.");
}
}
public static void main(String[] args) {
TaskManager manager = new TaskManager();
manager.addTask("Write unit tests for UserServlet");
manager.addTask("Fix NullPointerException in OrderService");
manager.addTask("Update README with API documentation");
manager.addTask("Review pull request #42");
// Complete a couple of tasks
manager.completeTask(2);
manager.completeTask(4);
// Show only pending tasks
manager.displayPendingTasks();
// Clean up completed tasks
manager.removeCompletedTasks();
// Show remaining tasks
manager.displayPendingTasks();
}
}
This example highlights several important patterns:
-
Encapsulating the ArrayList inside a class (the
TaskManager) rather than exposing it directly — this is a good practice because it lets you control how the list is modified. -
Using an
Iteratorto safely remove elements while iterating — callingtasks.remove()inside a for-each loop would throw aConcurrentModificationException. - Auto-incrementing IDs using a simple counter — this avoids duplicate identifiers without needing a database.
Common Mistakes to Avoid
After reviewing hundreds of Java programs, these are the most frequent ArrayList mistakes we see developers make:
Mistake 1: Using Raw Types Instead of Generics
// BAD: No type parameter — compiler cannot help you
List rawList = new ArrayList();
rawList.add("Hello");
rawList.add(42); // No compile error, but mixed types!
String value = (String) rawList.get(1); // ClassCastException at runtime
// GOOD: Type parameter catches errors at compile time
List<String> safeList = new ArrayList<>();
safeList.add("Hello");
// safeList.add(42); // Compile error — cannot add Integer
String value = safeList.get(0); // No cast needed
Mistake 2: Removing Elements During For-Each Iteration
List<String> items = new ArrayList<>();
items.add("A");
items.add("B");
items.add("C");
// BAD: Throws ConcurrentModificationException
for (String item : items) {
if (item.equals("B")) {
items.remove(item);
}
}
// GOOD Option 1: Use Iterator.remove()
Iterator<String> it = items.iterator();
while (it.hasNext()) {
if (it.next().equals("B")) {
it.remove();
}
}
// GOOD Option 2: Use removeIf() (Java 8+)
items.removeIf(item -> item.equals("B"));
Mistake 3: Accessing Indices That Do Not Exist
List<String> list = new ArrayList<>();
list.add("Only");
// BAD: Index 5 does not exist — throws IndexOutOfBoundsException
String s = list.get(5);
// GOOD: Validate the index before accessing
int index = 5;
if (index >= 0 && index < list.size()) {
String s = list.get(index);
} else {
System.out.println("Index out of range.");
}
Mistake 4: Confusing size() with Capacity
List<String> list = new ArrayList<>(); // backing array capacity = 10
list.add("A");
// size() returns 1 (actual number of elements)
// It does NOT return 10 (the internal array capacity)
System.out.println(list.size()); // prints 1, not 10
// If you need the capacity, you must cast to ArrayList
int capacity = ((ArrayList<?>) list).trimToSize();
// Note: there is no public getCapacity() method.
// trimToSize() shrinks the backing array to match size().
Best Practices
Following these guidelines will help you use ArrayList effectively and write cleaner code:
- Always specify the generic type. Never use raw types. Generics catch type errors at compile time rather than at runtime, making your programs safer and more readable.
-
Set an initial capacity when the size is predictable.
If you know a list will hold roughly 10,000 elements, create it as
new ArrayList<>(10_000). This avoids multiple resize operations during population. -
Program to the
Listinterface, not theArrayListclass. Declaring variables asList<T>makes it easy to swap implementations later without changing the rest of your code. -
Use
isEmpty()instead ofsize() == 0. Both work, butisEmpty()communicates intent more clearly and may be marginally faster on some implementations. -
Prefer
removeIf()for conditional removal (Java 8+). It is more readable than a manual iterator loop and less error-prone than the for-each approach. -
Use
Arrays.asList()orList.of()for fixed lists. If you do not need the list to be resizable, these factory methods are more concise and produce immutable lists (in the case ofList.of()). -
Call
trimToSize()after bulk removals. If you remove a large number of elements and know the list will not grow again, trimming frees the unused memory in the backing array. -
Do not use ArrayList in multi-threaded code without synchronization.
Use
Collections.synchronizedList(),CopyOnWriteArrayList, or explicit synchronization if multiple threads will modify the list concurrently.
Performance Considerations
Understanding the time complexity of ArrayList operations helps you make informed decisions and avoid subtle performance problems:
Time Complexity Summary
| Operation | Time Complexity | Notes |
|---|---|---|
get(index)
|
O(1) | Direct array access |
set(index, element)
|
O(1) | Direct array access |
add(element)
|
Amortized O(1) | O(n) only when resizing occurs |
add(index, element)
|
O(n) | Elements must be shifted right |
remove(index)
|
O(n) | Elements must be shifted left |
remove(object)
|
O(n) | Search + shift |
contains(object)
|
O(n) | Linear search |
indexOf(object)
|
O(n) | Linear search |
clear()
|
O(n) | Nulls out all references |
Memory Overhead
ArrayList consumes more memory than a plain array of the same logical size because:
-
The object header and internal fields (
int size,int modCount, and the array reference) add overhead. - The backing array may have unused slots (capacity > size), wasting memory.
- Each element stored in an ArrayList is a reference (4 or 8 bytes depending on the JVM), so the actual objects live elsewhere on the heap.
In memory-constrained environments, or when storing millions of primitive values, consider using specialized libraries like Trove or Eclipse Collections that offer primitive-aware lists, or stick with plain arrays.
ArrayList vs. LinkedList — A Practical Comparison
Developers sometimes assume LinkedList is always better for insertions and deletions. In practice, ArrayList outperforms LinkedList in most real-world scenarios due to better CPU cache locality — the backing array stores elements contiguously in memory, which modern CPUs handle efficiently. LinkedList nodes are scattered across the heap, causing more cache misses.
Use LinkedList only when you have a proven performance bottleneck involving frequent insertions or deletions at known positions within a very large list. Always measure with a profiler before switching.
Exercises
Apply what you have learned by completing these hands-on exercises. Try to solve each one before looking at the solution.
Exercise 1: Filter and Sum
Create an
ArrayList<Integer>
containing the numbers 1 through 20. Then:
- Remove all even numbers from the list.
- Calculate and print the sum of the remaining numbers.
- Print the final list.
Exercise 2: Merge Two Lists Without Duplicates
Given two
ArrayList<String>
objects, write a method that returns a new ArrayList containing all elements from both lists, but without any duplicate values. Preserve the order of first appearance.
Exercise 3: Employee Search
Create an
Employee
class with fields for name, department, and salary. Create an ArrayList of at least 8 employees across different departments. Then:
- Find and print all employees in a given department.
- Find the employee with the highest salary.
- Calculate the average salary per department.
Exercise 4: ArrayList as a Stack
Implement a simple stack (last-in, first-out) using only
ArrayList
. Your stack should support
push()
,
pop()
,
peek()
, and
isEmpty()
operations. Test it by pushing the values 10, 20, 30 and then popping them off one by one, printing each popped value.
Solutions
Solution to Exercise 1: Filter and Sum
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class FilterAndSum {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
for (int i = 1; i <= 20; i++) {
numbers.add(i);
}
// Remove even numbers using an Iterator
Iterator<Integer> iterator = numbers.iterator();
while (iterator.hasNext()) {
if (iterator.next() % 2 == 0) {
iterator.remove();
}
}
// Calculate the sum
int sum = 0;
for (int n : numbers) {
sum += n;
}
System.out.println("Remaining numbers: " + numbers);
System.out.println("Sum: " + sum);
}
}
The sum of all odd numbers from 1 to 19 is 100. The Iterator approach safely removes elements without triggering a
ConcurrentModificationException
. Alternatively, you could use
numbers.removeIf(n -> n % 2 == 0);
for a more concise solution.
Solution to Exercise 2: Merge Without Duplicates
import java.util.ArrayList;
import java.util.List;
public class MergeLists {
public static List<String> mergeWithoutDuplicates(
List<String> list1, List<String> list2) {
List<String> merged = new ArrayList<>();
for (String item : list1) {
if (!merged.contains(item)) {
merged.add(item);
}
}
for (String item : list2) {
if (!merged.contains(item)) {
merged.add(item);
}
}
return merged;
}
public static void main(String[] args) {
List<String> a = new ArrayList<>();
a.add("Java");
a.add("Python");
a.add("Go");
List<String> b = new ArrayList<>();
b.add("Python");
b.add("Rust");
b.add("Go");
List<String> result = mergeWithoutDuplicates(a, b);
System.out.println("Merged: " + result);
}
}
This approach preserves the order of first appearance. Note that
contains()
is O(n), so the overall method is O(n × m) where n and m are the sizes of the two lists. For very large lists, a
LinkedHashSet
would be more efficient because it provides O(1) contains checks while preserving insertion order.
Solution to Exercise 3: Employee Search
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Employee {
private String name;
private String department;
private double salary;
public Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
@Override
public String toString() {
return name + " | " + department + " | $" + salary;
}
}
public class EmployeeSearch {
public static void main(String[] args) {
List<Employee> employees = new ArrayList<>();
employees.add(new Employee("Alice", "Engineering", 95000));
employees.add(new Employee("Bob", "Marketing", 62000));
employees.add(new Employee("Carol", "Engineering", 110000));
employees.add(new Employee("Dave", "HR", 58000));
employees.add(new Employee("Eve", "Engineering", 88000));
employees.add(new Employee("Frank", "Marketing", 71000));
employees.add(new Employee("Grace", "HR", 64000));
employees.add(new Employee("Hank", "Engineering", 102000));
// 1. Find all employees in Engineering
System.out.println("--- Engineering Employees ---");
for (Employee emp : employees) {
if (emp.getDepartment().equals("Engineering")) {
System.out.println(emp);
}
}
// 2. Find the highest-paid employee
Employee highest = employees.get(0);
for (Employee emp : employees) {
if (emp.getSalary() > highest.getSalary()) {
highest = emp;
}
}
System.out.println("\nHighest paid: " + highest);
// 3. Average salary per department
Map<String, List<Double>> byDept = new HashMap<>();
for (Employee emp : employees) {
byDept.computeIfAbsent(
emp.getDepartment(), k -> new ArrayList<>())
.add(emp.getSalary());
}
System.out.println("\n--- Average Salary by Department ---");
for (Map.Entry<String, List<Double>> entry : byDept.entrySet()) {
double avg = entry.getValue().stream()
.mapToDouble(Double::doubleValue)
.average()
.orElse(0);
System.out.printf("%s: $%.2f%n", entry.getKey(), avg);
}
}
}
Solution to Exercise 4: ArrayList as a Stack
import java.util.ArrayList;
public class ArrayListStack {
private ArrayList<Integer> stack = new ArrayList<>();
public void push(int value) {
stack.add(value);
}
public int pop() {
if (stack.isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return stack.remove(stack.size() - 1);
}
public int peek() {
if (stack.isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return stack.get(stack.size() - 1);
}
public boolean isEmpty() {
return stack.isEmpty();
}
public static void main(String[] args) {
ArrayListStack stack = new ArrayListStack();
stack.push(10);
stack.push(20);
stack.push(30);
System.out.println("Top of stack: " + stack.peek());
System.out.println("Popped: " + stack.pop());
System.out.println("Popped: " + stack.pop());
System.out.println("Popped: " + stack.pop());
System.out.println("Is empty? " + stack.isEmpty());
}
}
This implementation uses the end of the ArrayList as the top of the stack.
push()
appends to the end (amortized O(1)), and
pop()
removes from the end (O(1) because no element shifting is needed). In production code, you would typically use
ArrayDeque
as a stack instead of
Stack
(which is legacy) or a custom ArrayList wrapper, but this exercise demonstrates how ArrayList can serve as the underlying data structure.
Summary
ArrayList is a versatile and widely-used collection class that provides a resizable array implementation of the
List
interface. Here are the key takeaways from this tutorial:
- ArrayList is backed by a regular Java array that grows automatically (by roughly 1.5×) when it runs out of space.
- It offers O(1) random access by index, making it excellent for read-heavy workloads.
- Insertions and deletions in the middle of the list are O(n) because elements must be shifted.
- Always use generics to ensure type safety and eliminate the need for casting.
-
Use an
IteratororremoveIf()when you need to remove elements during iteration. - Set an initial capacity when the expected size is known to avoid unnecessary resizing.
- ArrayList is not thread-safe — use synchronized wrappers or concurrent collections for multi-threaded scenarios.
- In most real-world cases, ArrayList outperforms LinkedList due to better cache locality.
Frequently Asked Questions
A regular Java array has a fixed size that cannot change after creation. If you allocate an array of length 5, it will always hold exactly 5 elements (or nulls). ArrayList, on the other hand, is a resizable array implementation that automatically grows when you add elements beyond its current capacity and can shrink when elements are removed.
Arrays can hold both primitives (int, double, etc.) and objects directly. ArrayList can only hold objects — when you add a primitive like an int, Java automatically boxes it into its wrapper class (Integer). Arrays also use special syntax with square brackets (
String[] arr
), while ArrayList is a class with methods like
add()
,
remove()
, and
get()
.
No, ArrayList is not thread-safe. If multiple threads modify the same ArrayList concurrently without external synchronization, the results are undefined — you may see inconsistent state, missed elements, or a
ConcurrentModificationException
.
To use a list safely across threads, you have several options: wrap the ArrayList with
Collections.synchronizedList()
, use
CopyOnWriteArrayList
from the
java.util.concurrent
package (which creates a new copy of the array on every write), or manually synchronize access using
synchronized
blocks. Each approach has different performance trade-offs, so choose based on your read-to-write ratio.
Use ArrayList when you need fast random access to elements by index and when you mostly add or remove elements at the end of the list. ArrayList provides O(1) random access but O(n) insertion and deletion in the middle due to element shifting.
Use LinkedList when you frequently insert or delete elements at known positions within the list, as these operations are O(1) for LinkedList (once you have a reference to the position) versus O(n) for ArrayList. However, in practice, ArrayList is faster than LinkedList for most real-world workloads because its backing array has much better CPU cache locality — contiguous memory access is significantly faster than following pointers between scattered nodes. Always measure with a profiler before switching from ArrayList to LinkedList based on theoretical complexity alone.
Yes, ArrayList can contain null values. You can add null as an element using
add(null)
, and the list can hold multiple null values since it allows duplicates. However, you need to be careful when working with null elements — calling methods on a null element will throw a
NullPointerException
, and operations like
sort(null)
(natural ordering) will fail if the list contains nulls because comparing null with a non-null value is not defined for most Comparable types.
When you add an element and the backing array is full, the ArrayList automatically increases its capacity. It creates a new array that is typically 1.5 times the size of the current array (the exact formula is
newCapacity = oldCapacity + (oldCapacity >> 1)
), copies all existing elements from the old array into the new one, and then inserts the new element. The old array becomes eligible for garbage collection.
This resize operation takes O(n) time where n is the number of existing elements, which is why it is called "amortized O(1)" for add — most adds are O(1), but occasionally one triggers a resize and costs O(n). If you know approximately how many elements the list will hold, setting an initial capacity with
new ArrayList<>(estimatedSize)
avoids these resizes entirely.
The recommended approach is to call
toArray(T[] array)
with a zero-length array of the desired type:
String[] array = list.toArray(new String[0]);
This works because the
toArray
method checks if the provided array is large enough. If it is not (which is the case with a zero-length array), the method allocates a new array of the correct size and returns it. The alternative — passing a pre-sized array like
new String[list.size()]
— also works but is slightly more verbose and creates an array that will be discarded if the JVM internally allocates a different one anyway.