
ArrayList in Java
ArrayList is similar to a regular array except that the size is dynamically adjusted as the number of items in the collection changes.
July 7, 2024 · 11 min read
An ArrayList is similar to a regular array except that the size is dynamically adjusted as the number of items in the collection changes. Arrays have a fixed size that cannot change. But with ArrayList, you don't need to worry about the size.
Key Properties of ArrayList
The initial capacity of ArrayList is 10 — since Java 8, the backing array isn't actually allocated until the first element is added, so an empty ArrayList costs almost nothing.
ArrayList is best suited when we have read-heavy operations. However, if you're required to insert or delete elements in the middle of the collection, ArrayList is not preferred.
ArrayList is not synchronized (not thread-safe).
| Operation | Time Complexity |
|---|---|
| Access item | O(1) time complexity |
| Adding items | O(1) on average, but can be O(n) when resizing is needed |
| Removing items | O(n) because it may require shifting elements. |
How resizing actually works
ArrayList is backed by a plain Object[]. When add() finds the array full, it doesn't double it — it grows to oldCapacity + (oldCapacity >> 1), roughly 1.5x. That's a deliberate trade-off against wasted memory; Vector doubles by default instead, which we'll cover in the Vector article.
Every resize allocates a new array and copies every existing element into it with Arrays.copyOf. If you already know roughly how many elements you'll store, pass that as the initial capacity during intiailization (new ArrayList<>(1000)) to skip the repeated copying entirely.
Mutation while iterating
ArrayList's iterator is fail-fast: it checks an internal modCount on every next() call and throws ConcurrentModificationException if the list was structurally changed by anything other than the iterator itself. This means:
for (String fruit : list1) {
if (fruit.equals("Banana")) {
list1.remove(fruit); // throws ConcurrentModificationException
}
}Use Iterator.remove() or list1.removeIf(...) instead — both update modCount in a way the iterator expects, so no exception is thrown.
Creating and initializing ArrayList
Let's look at how to use an ArrayList.
//Using the Default Constructor
List<String> list1 = new ArrayList<>();
list1.add("Apple");
list1.add("Banana");
// Using Constructor with Initial Capacity
List<String> list2 = new ArrayList<>(20);
list2.add("Apple");
list2.add("Banana");
//Initializing array list using the anonymous inner class method
List<String> list3 = new ArrayList<String>() {{
add("Apple");
add("Banana");
add("Orange");
}};
// fixed-size list from the elements of another collection
List<String> list4 = Arrays.asList("Apple", "Banana", "Orange");
List<String> list5 = new ArrayList<>(list4);
// Using Arrays.asList method
List<String> list6 = new ArrayList<>(Arrays.asList("Apple", "Banana", "Orange"));
// Using Collections.addAll method
List<String> list7 = new ArrayList<>();
Collections.addAll(list7, "Apple", "Banana", "Orange");
// Using Java 8 streams and collectors
List<String> list8 = Stream.of("Apple", "Banana", "Orange")
.collect(Collectors.toCollection(ArrayList::new));Items inside an ArrayList can be accessed using their index.
list1.get(1);Items inside an ArrayList can be changed using the set() method, which takes the index and the new value. This replaces the element at the specified position.
list1.set(1, "Guava");To find out how many elements an ArrayList holds, use the size() method.
list1.size();To check whether an item exists in an ArrayList, use the contains() method. It returns true or false.
list1.contains("Banana");Iterate ArrayList Items
There are multiple ways we can iterate an ArrayList.
// Iterate using enhanced for loop
for (String item:list1) {
System.out.println(item);
}
// Iterate using a traditional for loop
for (int i = 0; i < list1.size(); i++) {
System.out.println(list1.get(i));
}
// Using an Iterator
Iterator<String> iterator = list1.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
// Using ListIterator
ListIterator<String> listIterator = list1.listIterator();
while (listIterator.hasNext()) {
System.out.println(listIterator.next());
}
// Using a Java8 Stream and lambda expression
list1.stream().forEach(item -> System.out.println(item));
// Using a Java8 forEach method
list1.forEach(item -> System.out.println(item));Note
The Iterator interface provides methods to iterate a collection. The iterator generally traverse elements one by one in a forward direction. However, the ListIterator is specific to lists. It offers bidirectional traversal and modification.
Removing Item in ArrayList
We can remove an item from an ArrayList using the remove() method, either by index or by value.
// Removes the element at index 1
list1.remove(1);
// Removes the first occurrence of "Banana"
list1.remove("Banana");
// Removes all items found in another collection
List<String> toRemove = Arrays.asList("Banana", "Orange");
list1.removeAll(toRemove);
// Removes elements matching a condition
list1.removeIf(fruit -> fruit.startsWith("B"));
// Removes all elements from the list
list1.clear();Creating Immutable List in Java
To create an immutable list using the List.of() factory method.
public class ImmutableList {
public static void main(String[] args) {
List<String> immutableList = List.of("A", "B", "C");
System.out.println(immutableList);
immutableList.add("D"); // throws UnsupportedOperationException
System.out.println(immutableList);
}
}We can also create an unmodifiable view of the list using the Collections.unmodifiableList() method. If you modify the returned list, whether directly or via its iterator will result in an UnsupportedOperationException.
public class UnmodifiableListExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
List<String> unmodifiableList = Collections.unmodifiableList(list);
System.out.println(unmodifiableList);
unmodifiableList.add("D"); // throws UnsupportedOperationException
System.out.println(unmodifiableList);
}
}ArrayList is not Synchronized
ArrayList is not synchronized. What it means, is when multiple threads attempt to modify the same list simultaneously, it can lead to unpredictable behaviour.
Let us look into the following program:
public class NonSynchronizedArrayList {
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<>();
// Create a runnable task that adds elements to the list
Runnable addItemsTask = () -> {
for (int i = 0; i < 1000; i++) {
arrayList.add(i);
}
};
// Create multiple threads that will run the addItemsTask
Thread thread1 = new Thread(addItemsTask);
thread1.start();
Thread thread2 = new Thread(addItemsTask);
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
System.out.println("Size of List: " + arrayList.size());
}
}In this example, you expect the size of the list to be 2000, but it will be less than 2000. This is because multiple threads are interfering with each other which leads to corrupted data.
To make the ArrayList thread-safe, we can use Collections.synchronizedList() method. The synchronizedList() method returns a synchronized (thread-safe) list backed by the specified list.
To guarantee serial access, all access to the backing list must be accomplished through the returned list. The above program can be written as follows:
public class SynchronizedArrayList {
public static void main(String[] args) {
List<Integer> arrayList = Collections.synchronizedList(new ArrayList<>());
// Create a runnable task that adds elements to the list
Runnable addItemsTask = () -> {
for (int i = 0; i < 1000; i++) {
arrayList.add(i);
}
};
// Create multiple threads that will run the addItemsTask
Thread thread1 = new Thread(addItemsTask);
thread1.start();
Thread thread2 = new Thread(addItemsTask);
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
System.out.println("Size of List: " + arrayList.size());
}
}ArrayList vs the Alternatives
- vs
LinkedList:ArrayListwins for random access (get(i)is O(1) vs O(n)) and for cache locality — elements sit contiguously in memory, so iteration is faster in practice even where both are O(n) on paper.LinkedListonly pulls ahead when you're inserting or removing at a position you already hold via aListIterator, since that's O(1) with no shifting. - vs
ArrayDeque: if you only ever add/remove at the ends,ArrayDequeis faster and lighter thanArrayListfor that access pattern — no shifting of interior elements, and it doesn't need to keep the array fully packed. - vs
CopyOnWriteArrayList: reach forArrayListin single-threaded code or when you'll synchronize externally yourself.CopyOnWriteArrayListonly pays off when reads vastly outnumber writes.
