Java Tutorial
🔍

Java ArrayList

Java ArrayList

ArrayList is the most-used collection in Java. It is a resizable array — you can add as many elements as you want without declaring a size upfront, and it still gives you O(1) random access by index just like a plain array. Most Java applications have more ArrayList instances than any other data structure, and most entry-level interview questions on collections start here.

What Is ArrayList?

ArrayList<E> is a concrete implementation of the List interface that stores elements in a dynamically resizing Object[] array. It lives in java.util and has been part of Java since version 1.2 when the Collections Framework was introduced.

The simplest way to understand it: ArrayList is a plain Java array with automatic growth. When you add an element that would exceed the current capacity, it creates a larger array, copies everything across, and continues — all invisibly.

The diagram below shows where ArrayList sits in the Collections hierarchy.

java.lang.Iterable
    └── java.util.Collection
            └── java.util.List          (interface)
                    ├── java.util.ArrayList    (resizable array — most common)
                    ├── java.util.LinkedList   (doubly linked nodes)
                    └── java.util.Vector       (legacy, synchronized)

ArrayList implements List, RandomAccess, Cloneable, and Serializable. The RandomAccess marker interface is what allows algorithms like Collections.sort() and Collections.binarySearch() to use index-based access instead of iterator-based traversal when they detect it.

Key Properties at a Glance

  • Preserves insertion order — elements come out in the order you put them in
  • Allows duplicate elements
  • Allows a single null element — or multiple, actually; there is no limit
  • Not thread-safe — concurrent modification from two threads produces unpredictable results
  • Backed by an Object[] internally; type safety is enforced via generics at compile time only

When to Use ArrayList

ArrayList is the correct default List for most situations. Choose it when:

  • Elements need to be accessed by index frequently — get(index) is O(1)
  • The list is built by appending elements to the end — add(element) is O(1) amortised
  • You iterate sequentially through the entire list — cache-friendly contiguous memory
  • The list is used in a single thread with no concurrent modification

Switch to LinkedList only when insertions and removals at the head of a large list are the primary operation. LinkedList.addFirst() and removeFirst() are O(1), while ArrayList must shift elements right by one position for each such operation — O(n).

Switch to HashSet when the primary operation is contains() on a large dataset. ArrayList.contains() is O(n). HashSet.contains() is O(1). A mistake that appears often in fresher pull requests is using List.contains() inside a loop on a list of thousands of items — this creates an O(n²) algorithm that could be O(n) with a Set.

How ArrayList Works Internally

Understanding ArrayList's internal mechanism is what separates a developer who uses it from one who can reason about it under pressure — including in interviews.

The Backing Array

Internally, ArrayList maintains a single field named elementData of type Object[]. This array is the actual store. The field size tracks how many elements are currently held — it is always less than or equal to elementData.length, which is the capacity.

After ArrayList<String> list = new ArrayList<>()
and list.add("A"), list.add("B"), list.add("C"):

  elementData:  [ "A" | "B" | "C" | null | null | null | null | null | null | null ]
                   0      1      2      3      4      5      6      7      8      9
  size: 3
  capacity: 10  (default initial capacity)

  Positions 0..2 are live elements.
  Positions 3..9 are allocated but unused — they hold null references.

The default initial capacity is 10. If you call new ArrayList<>(), Java allocates an array of length 10 (actually an empty array that defers to 10 on the first add, but the effect is the same).

The Growth Formula

When size == elementData.length and a new element is added, ArrayList must grow. The growth formula is:

newCapacity = oldCapacity + (oldCapacity >> 1)
            = oldCapacity + oldCapacity / 2
            = oldCapacity * 1.5  (approximately)

So the capacity sequence is: 10 → 15 → 22 → 33 → 49 → 73 → 109 → ...

Each resize allocates a new array of the larger size and copies all existing elements using System.arraycopy(). This is an O(n) operation that happens infrequently — and because it doubles roughly, the total work for n insertions is still O(n) amortised.

Why Pre-Sizing Matters

If you know you are about to add 10,000 elements, use new ArrayList<>(10_000). This allocates one array of that capacity upfront and eliminates all intermediate resize operations. Each resize copies all existing elements — multiple resizes during a bulk load wastes time and creates GC pressure from the discarded intermediate arrays.

1// File: ArrayListGrowthDemo.java 2 3import java.util.ArrayList; 4import java.lang.reflect.Field; 5 6public class ArrayListGrowthDemo { 7 8 // Access the internal capacity via reflection — for educational purposes only 9 static int capacity(ArrayList<?> list) throws Exception { 10 Field field = ArrayList.class.getDeclaredField("elementData"); 11 field.setAccessible(true); 12 return ((Object[]) field.get(list)).length; 13 } 14 15 public static void main(String[] args) throws Exception { 16 17 ArrayList<Integer> list = new ArrayList<>(); 18 19 // Track capacity changes as elements are added 20 int previousCapacity = capacity(list); 21 System.out.printf("%-10s %-10s %-10s%n", "Size", "Capacity", "Grew?"); 22 System.out.println("-".repeat(32)); 23 24 for (int i = 1; i <= 25; i++) { 25 list.add(i); 26 int currentCapacity = capacity(list); 27 if (currentCapacity != previousCapacity) { 28 System.out.printf("%-10d %-10d %-10s%n", 29 list.size(), currentCapacity, "YES — resize triggered"); 30 previousCapacity = currentCapacity; 31 } else { 32 System.out.printf("%-10d %-10d%n", list.size(), currentCapacity); 33 } 34 } 35 } 36}
Output:
Size       Capacity   Grew?
--------------------------------
1          10
2          10
...
10         10
11         15         YES — resize triggered
...
15         15
16         22         YES — resize triggered
...
22         22
23         33         YES — resize triggered
...
25         33

Core Operations with Examples

Adding Elements

add(E element) appends to the end in O(1) amortised. add(int index, E element) inserts at a position in O(n) — every element from index to size-1 shifts one position right.

1// File: ArrayListAddDemo.java 2 3import java.util.ArrayList; 4import java.util.List; 5 6public class ArrayListAddDemo { 7 8 public static void main(String[] args) { 9 10 List<String> products = new ArrayList<>(); 11 12 // Append to the end — O(1) amortised 13 products.add("Laptop"); 14 products.add("Mouse"); 15 products.add("Keyboard"); 16 System.out.println("After appending: " + products); 17 18 // Insert at index 1 — shifts Mouse and Keyboard right — O(n) 19 products.add(1, "Monitor"); 20 System.out.println("After insert at index 1: " + products); 21 22 // addAll — appends an entire collection 23 products.addAll(List.of("Webcam", "Headset")); 24 System.out.println("After addAll: " + products); 25 System.out.println("Size: " + products.size()); 26 } 27}
Output:
After appending: [Laptop, Mouse, Keyboard]
After insert at index 1: [Laptop, Monitor, Mouse, Keyboard]
After addAll: [Laptop, Monitor, Mouse, Keyboard, Webcam, Headset]
Size: 6

Accessing and Updating Elements

get(int index) is O(1) — it reads directly from the backing array at that position. set(int index, E element) replaces the element and returns the old one.

1// File: ArrayListAccessDemo.java 2 3import java.util.ArrayList; 4import java.util.List; 5 6public class ArrayListAccessDemo { 7 8 public static void main(String[] args) { 9 10 List<String> cities = new ArrayList<>( 11 List.of("Mumbai", "Delhi", "Bengaluru", "Chennai", "Hyderabad") 12 ); 13 14 // Direct index access — O(1) 15 System.out.println("First city : " + cities.get(0)); 16 System.out.println("Third city : " + cities.get(2)); 17 System.out.println("Last city : " + cities.get(cities.size() - 1)); 18 19 // Replace an element — returns the old value 20 String replaced = cities.set(2, "Pune"); 21 System.out.println("Replaced : " + replaced + " with Pune"); 22 System.out.println("Updated list: " + cities); 23 24 // Search — O(n) linear scan 25 System.out.println("Index of Delhi: " + cities.indexOf("Delhi")); 26 System.out.println("Contains Pune : " + cities.contains("Pune")); 27 } 28}
Output:
First city : Mumbai
Third city : Bengaluru
Last city  : Bengaluru
Replaced   : Bengaluru with Pune
Updated list: [Mumbai, Delhi, Pune, Chennai, Hyderabad]
Index of Delhi: 1
Contains Pune : true

Removing Elements

There are two remove overloads that beginners frequently confuse: remove(int index) removes by position, and remove(Object o) removes by value. On a List<Integer>, this distinction causes a real bug.

1// File: ArrayListRemoveDemo.java 2 3import java.util.ArrayList; 4import java.util.List; 5 6public class ArrayListRemoveDemo { 7 8 public static void main(String[] args) { 9 10 List<String> orders = new ArrayList<>( 11 List.of("ORD-001", "ORD-002", "ORD-003", "ORD-004", "ORD-005") 12 ); 13 14 // remove by index — removes ORD-003 (index 2) — O(n) due to shift 15 orders.remove(2); 16 System.out.println("After remove(index 2): " + orders); 17 18 // remove by value — removes ORD-004 — O(n) linear scan + shift 19 orders.remove("ORD-004"); 20 System.out.println("After remove(ORD-004): " + orders); 21 22 // removeIf — safe removal during iteration using a predicate 23 orders.removeIf(order -> order.endsWith("1")); 24 System.out.println("After removeIf (ends with 1): " + orders); 25 26 // The integer overload trap 27 List<Integer> scores = new ArrayList<>(List.of(10, 20, 30, 40)); 28 scores.remove(1); // removes by INDEX 1, not the value 1 29 System.out.println("\nscores after remove(1) as index: " + scores); 30 scores.remove(Integer.valueOf(30)); // removes by VALUE — must box the int 31 System.out.println("scores after remove(Integer 30): " + scores); 32 } 33}
Output:
After remove(index 2): [ORD-001, ORD-002, ORD-004, ORD-005]
After remove(ORD-004): [ORD-001, ORD-002, ORD-005]
After removeIf (ends with 1): [ORD-002, ORD-005]

scores after remove(1) as index: [10, 30, 40]
scores after remove(Integer 30): [10, 40]

Iterating Elements

Three standard patterns exist: for-each, index loop, and Iterator. For-each is cleanest for read-only traversal. The explicit Iterator is required when removing during traversal.

1// File: ArrayListIterationDemo.java 2 3import java.util.ArrayList; 4import java.util.Iterator; 5import java.util.List; 6 7public class ArrayListIterationDemo { 8 9 public static void main(String[] args) { 10 11 List<String> languages = new ArrayList<>( 12 List.of("Java", "Python", "Go", "Rust", "Kotlin") 13 ); 14 15 // For-each — cleanest for read-only 16 System.out.print("For-each: "); 17 for (String lang : languages) { 18 System.out.print(lang + " "); 19 } 20 System.out.println(); 21 22 // Index loop — needed when position matters 23 System.out.println("Index loop:"); 24 for (int i = 0; i < languages.size(); i++) { 25 System.out.printf(" [%d] %s%n", i, languages.get(i)); 26 } 27 28 // Iterator — the only safe way to remove during traversal 29 System.out.println("Before iterator removal: " + languages); 30 Iterator<String> it = languages.iterator(); 31 while (it.hasNext()) { 32 String lang = it.next(); 33 if (lang.length() > 4) { 34 it.remove(); // safe — iterator tracks the structural change 35 } 36 } 37 System.out.println("After removing names longer than 4 chars: " + languages); 38 39 // Modern alternative: removeIf 40 List<String> copy = new ArrayList<>(List.of("Java", "Python", "Go", "Rust")); 41 copy.removeIf(s -> s.length() > 4); 42 System.out.println("removeIf equivalent: " + copy); 43 } 44}
Output:
For-each: Java Python Go Rust Kotlin
Index loop:
  [0] Java
  [1] Python
  [2] Go
  [3] Rust
  [4] Kotlin
Before iterator removal: [Java, Python, Go, Rust, Kotlin]
After removing names longer than 4 chars: [Java, Go, Rust]
removeIf equivalent: [Java, Go, Rust]

Sorting and Searching

Collections.sort() and list.sort() both work on ArrayList. Binary search requires the list to be sorted first.

1// File: ArrayListSortSearchDemo.java 2 3import java.util.ArrayList; 4import java.util.Collections; 5import java.util.Comparator; 6import java.util.List; 7 8public class ArrayListSortSearchDemo { 9 10 public static void main(String[] args) { 11 12 List<Integer> prices = new ArrayList<>(List.of(4999, 999, 15999, 599, 7499)); 13 System.out.println("Original : " + prices); 14 15 // Natural ascending sort 16 Collections.sort(prices); 17 System.out.println("Ascending : " + prices); 18 19 // Descending sort using Comparator 20 prices.sort(Comparator.reverseOrder()); 21 System.out.println("Descending: " + prices); 22 23 // Binary search — list must be sorted ascending first 24 Collections.sort(prices); 25 int index = Collections.binarySearch(prices, 7499); 26 System.out.println("binarySearch(7499): index = " + index); 27 28 // Custom object sort 29 List<String> names = new ArrayList<>(List.of("Rohan", "Priya", "Ananya", "Karan")); 30 names.sort(Comparator.naturalOrder()); 31 System.out.println("Names sorted: " + names); 32 names.sort(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder())); 33 System.out.println("Names by length: " + names); 34 } 35}
Output:
Original  : [4999, 999, 15999, 599, 7499]
Ascending : [599, 999, 4999, 7499, 15999]
Descending: [15999, 7499, 4999, 999, 599]
binarySearch(7499): index = 3
Names sorted: [Ananya, Karan, Priya, Rohan]
Names by length: [Karan, Priya, Rohan, Ananya]

Real-World Example — Swiggy Cart Management Service

A food delivery platform like Swiggy manages a cart per user session. The cart holds items in addition order, allows duplicates (two biryanis for a large order), supports item-level removal, and calculates totals. ArrayList fits this exactly: ordered, duplicates allowed, index-based access for display, and no concurrent modification since the cart belongs to one user session.

1// File: CartItem.java 2 3public class CartItem { 4 5 private final String itemId; 6 private final String name; 7 private final double price; 8 private int quantity; 9 10 public CartItem(String itemId, String name, double price, int quantity) { 11 this.itemId = itemId; 12 this.name = name; 13 this.price = price; 14 this.quantity = quantity; 15 } 16 17 public String getItemId() { return itemId; } 18 public String getName() { return name; } 19 public double getPrice() { return price; } 20 public int getQuantity(){ return quantity; } 21 public void setQuantity(int qty) { this.quantity = qty; } 22 23 public double subtotal() { return price * quantity; } 24 25 @Override 26 public String toString() { 27 return String.format("%-20s x%d Rs.%.2f", name, quantity, subtotal()); 28 } 29}
1// File: CartService.java 2 3import java.util.ArrayList; 4import java.util.List; 5import java.util.Optional; 6 7public class CartService { 8 9 private final List<CartItem> items = new ArrayList<>(); 10 11 // Adds a new item or increments quantity if item already exists 12 public void addItem(CartItem newItem) { 13 Optional<CartItem> existing = items.stream() 14 .filter(item -> item.getItemId().equals(newItem.getItemId())) 15 .findFirst(); 16 17 if (existing.isPresent()) { 18 // Item already in cart — increase quantity instead of adding duplicate entry 19 existing.get().setQuantity(existing.get().getQuantity() + newItem.getQuantity()); 20 } else { 21 items.add(newItem); 22 } 23 } 24 25 // Removes all entries for a given item ID 26 public boolean removeItem(String itemId) { 27 return items.removeIf(item -> item.getItemId().equals(itemId)); 28 } 29 30 // Retrieves item by cart position — O(1) because ArrayList has index access 31 public CartItem getItemAtPosition(int position) { 32 if (position < 0 || position >= items.size()) { 33 throw new IndexOutOfBoundsException("No item at cart position: " + position); 34 } 35 return items.get(position); 36 } 37 38 public double totalAmount() { 39 return items.stream().mapToDouble(CartItem::subtotal).sum(); 40 } 41 42 public int totalItems() { 43 return items.stream().mapToInt(CartItem::getQuantity).sum(); 44 } 45 46 public void printCart() { 47 System.out.println("=".repeat(44)); 48 System.out.println(" SWIGGY CART"); 49 System.out.println("=".repeat(44)); 50 if (items.isEmpty()) { 51 System.out.println(" Your cart is empty."); 52 } else { 53 for (int i = 0; i < items.size(); i++) { 54 System.out.printf(" %d. %s%n", i + 1, items.get(i)); 55 } 56 System.out.println("-".repeat(44)); 57 System.out.printf(" Total items: %d | Total: Rs.%.2f%n", 58 totalItems(), totalAmount()); 59 } 60 System.out.println("=".repeat(44)); 61 } 62}
1// File: CartDemo.java 2 3public class CartDemo { 4 5 public static void main(String[] args) { 6 7 CartService cart = new CartService(); 8 9 // User adds items 10 cart.addItem(new CartItem("I001", "Chicken Biryani", 349.00, 2)); 11 cart.addItem(new CartItem("I002", "Paneer Butter Masala", 279.00, 1)); 12 cart.addItem(new CartItem("I003", "Butter Naan", 49.00, 4)); 13 cart.printCart(); 14 15 // User adds another Biryani — should increment quantity, not create duplicate 16 cart.addItem(new CartItem("I001", "Chicken Biryani", 349.00, 1)); 17 System.out.println("\nAfter adding one more Biryani:"); 18 cart.printCart(); 19 20 // User removes Naan 21 cart.removeItem("I003"); 22 System.out.println("\nAfter removing Butter Naan:"); 23 cart.printCart(); 24 25 // Access specific cart position — O(1) 26 CartItem first = cart.getItemAtPosition(0); 27 System.out.println("\nFirst item in cart: " + first.getName()); 28 } 29}
Output:
============================================
  SWIGGY CART
============================================
  1. Chicken Biryani      x2  Rs.698.00
  2. Paneer Butter Masala x1  Rs.279.00
  3. Butter Naan          x4  Rs.196.00
--------------------------------------------
  Total items: 7  |  Total: Rs.1173.00
============================================

After adding one more Biryani:
============================================
  SWIGGY CART
============================================
  1. Chicken Biryani      x3  Rs.1047.00
  2. Paneer Butter Masala x1  Rs.279.00
  3. Butter Naan          x4  Rs.196.00
--------------------------------------------
  Total items: 8  |  Total: Rs.1522.00
============================================

After removing Butter Naan:
============================================
  SWIGGY CART
============================================
  1. Chicken Biryani      x3  Rs.1047.00
  2. Paneer Butter Masala x1  Rs.279.00
--------------------------------------------
  Total items: 4  |  Total: Rs.1326.00
============================================

First item in cart: Chicken Biryani

This example demonstrates why ArrayList is the correct choice here: items display in addition order, duplicate items are handled by quantity tracking rather than separate entries, and position-based access (getItemAtPosition) is genuinely O(1).

Performance Considerations

OperationTime ComplexityNotes
add(E element)O(1) amortisedO(n) when resize is triggered
add(int index, E element)O(n)Shifts all elements right of index
get(int index)O(1)Direct array lookup — fastest possible
set(int index, E element)O(1)Direct array write
remove(int index)O(n)Shifts all elements left of index
remove(Object o)O(n)Linear scan to find + shift to close gap
contains(Object o)O(n)Linear scan — use HashSet for O(1)
size()O(1)Returns the size field directly
sort()O(n log n)TimSort — stable, efficient for nearly-sorted data
Iteration (for-each / iterator)O(n)Cache-friendly contiguous access

Memory footprint: The backing array holds object references (4 or 8 bytes each, depending on JVM pointer compression). The list itself has minimal overhead — about 40 bytes plus the array. Unused capacity holds null references, so an ArrayList pre-sized to 10,000 that holds 100 elements wastes ~9,900 reference slots but no actual object memory.

Thread safety: ArrayList is not thread-safe. For concurrent read-heavy use, wrap it: Collections.synchronizedList(new ArrayList<>()). For better concurrency, prefer CopyOnWriteArrayList from java.util.concurrent when writes are rare and reads are frequent.

Best Practices

Always declare the variable type as List<E>, not ArrayList<E>. Write List<String> names = new ArrayList<>() rather than ArrayList<String> names = new ArrayList<>(). This lets you swap the implementation to LinkedList or CopyOnWriteArrayList later without touching any other code. During code reviews, seniors flag ArrayList on the left side as a sign that the developer is coupled to an implementation rather than an interface.

Pre-size when the expected element count is known. new ArrayList<>(expectedSize) avoids all intermediate resizes. This matters when loading thousands of rows from a database or reading a large CSV file. The cost of a single pre-size call is trivially small compared to the cost of multiple O(n) copy operations during bulk loading.

Use removeIf(predicate) for conditional removal rather than iterating with an explicit iterator. removeIf is cleaner, handles the internal modCount bookkeeping correctly, and makes the intent clear. It was added in Java 8 and should be the default choice in any codebase running Java 8+.

Use subList() for range operations instead of looping. list.subList(from, to) returns a live view — operations on it reflect in the original list. list.subList(2, 5).clear() removes elements 2, 3, 4 cleanly and efficiently without a manual loop.

Common Mistakes

Mistake 1 — Modifying a List Inside a for-each Loop

1List<String> items = new ArrayList<>(List.of("A", "B", "C", "D")); 2 3// This throws ConcurrentModificationException at runtime — not a compile error 4for (String item : items) { 5 if (item.equals("B")) { 6 items.remove(item); // modifies list while iterator is active 7 } 8}

The for-each loop uses an Iterator internally. ArrayList's iterator is fail-fast — it tracks a modCount field that increments on every structural change. When remove() increments modCount and the iterator's expectedModCount no longer matches, it throws. The fix is items.removeIf("B"::equals) or an explicit Iterator.remove() call.

Mistake 2 — Confusing remove(int) and remove(Object) on List

1List<Integer> numbers = new ArrayList<>(List.of(10, 20, 30, 40)); 2 3numbers.remove(2); // removes element at INDEX 2 (value 30) 4numbers.remove(Integer.valueOf(10)); // removes the VALUE 10 5 6// Without Integer.valueOf(), calling remove(10) on a large list still works 7// because autoboxing happens — but calling remove(0), remove(1), etc. on a 8// List<Integer> always resolves to the index overload, not the value overload.

This is one of the most common List<Integer> bugs. Always use Integer.valueOf(n) or cast (Integer) n when you mean to remove by value.

Mistake 3 — Using ArrayList.contains() in a Loop for Large Lists

1// O(n²) — contains is O(n), called n times 2List<String> processedIds = new ArrayList<>(); 3for (String id : incomingIds) { 4 if (!processedIds.contains(id)) { // linear scan on every iteration 5 processedIds.add(id); 6 process(id); 7 } 8} 9 10// O(n) — Set.add() returns false if already present; contains is O(1) 11Set<String> processedIds = new HashSet<>(); 12for (String id : incomingIds) { 13 if (processedIds.add(id)) { // O(1) — add returns false for duplicates 14 process(id); 15 } 16}

Mistake 4 — Not Handling IndexOutOfBoundsException on Dynamic Data

1// Fragile — assumes list has at least 3 elements 2String third = myList.get(2); 3 4// Defensive — verify size first or use a conditional 5String third = myList.size() > 2 ? myList.get(2) : null;

Production lists come from database queries and API responses whose result counts are not guaranteed at compile time. A get() call on an empty or short list is a runtime exception waiting to happen. Check size() or use isEmpty() before accessing by position.

Interview Questions

Q1. What is ArrayList in Java and how is it different from a plain array?

ArrayList is a resizable-array implementation of the List interface. A plain Java array has a fixed size set at creation — you cannot add beyond that without creating a new array manually. ArrayList handles growth automatically by allocating a larger array and copying elements when capacity is exhausted. It also provides a rich API — add, remove, sort, contains — that arrays do not. The trade-off is that ArrayList stores objects (or boxed primitives), while primitive arrays store primitives directly, making arrays faster and more memory-efficient for fixed-size numeric data.

Q2. How does ArrayList grow when it runs out of space?

When size == capacity and a new element is added, ArrayList computes newCapacity = oldCapacity + (oldCapacity >> 1), which is approximately 1.5 times the old capacity. It allocates a new Object[] of that length, copies all existing elements using System.arraycopy(), and replaces the internal reference. The old array becomes eligible for garbage collection. The default initial capacity is 10, so the growth sequence is 10 → 15 → 22 → 33 → 49. Interviewers at product companies often follow this up with: "So how would you prevent this overhead for a bulk load?" — the answer is pre-sizing with new ArrayList<>(expectedSize).

Q3. What is the time complexity of add(), get(), and remove() on ArrayList?

add(E) at the end is O(1) amortised — O(n) during resize, but resizes become exponentially rarer as the list grows, making the average O(1). get(int index) is O(1) — direct array index access. remove(int index) is O(n) because every element to the right of the removed position must shift one position left using System.arraycopy(). contains() and remove(Object) are both O(n) — they perform a linear scan. This is why ArrayList is unsuitable when contains() is called frequently on large lists.

Q4. What is ConcurrentModificationException and when does ArrayList throw it?

ArrayList's iterator is fail-fast. It records the list's internal modCount (a counter incremented on every structural change) at creation time as expectedModCount. On every next() call, it checks whether modCount == expectedModCount. If the list was structurally modified outside the iterator — by calling list.add() or list.remove() directly during a for-each loop — modCount changes and the iterator throws ConcurrentModificationException. This is a programming error detection mechanism, not a thread-safety guarantee. Fix it by using iterator.remove() or list.removeIf() instead of direct modification.

Q5. When would you choose LinkedList over ArrayList?

LinkedList is faster when the primary operation is inserting or removing elements at the head of the list — addFirst() and removeFirst() are O(1) compared to ArrayList's O(n) shift. For a large list used as a queue where elements are always added at the tail and removed from the head, LinkedList is a reasonable choice. However, for almost all other access patterns — random access, iteration, sorting — ArrayList is faster due to cache-friendly contiguous memory. LinkedList also uses significantly more memory: each element requires a Node object with prev and next references, roughly 24-32 bytes of overhead per element versus 4-8 bytes for an ArrayList reference.

Q6. What is the difference between ArrayList and Vector?

Both are resizable-array List implementations. Vector synchronises every public method, making it thread-safe but slow even in single-threaded code. ArrayList is not synchronised — faster for single-threaded use. Vector doubles its capacity on resize; ArrayList grows by 50%. Vector is a legacy class from Java 1.0, predating the Collections Framework. ArrayList was introduced in Java 1.2 as its non-synchronised replacement. In modern code, ArrayList is always preferred. For thread safety, use Collections.synchronizedList(new ArrayList<>()) or CopyOnWriteArrayList rather than Vector.

FAQs

How do I convert an ArrayList to an array in Java?

Use list.toArray(new String[0]) for a typed array. The zero-length array argument tells the JVM the target type — Java allocates the correctly-sized array internally. Passing new String[list.size()] also works but is slightly less efficient on modern JVMs. list.toArray() without an argument returns Object[], which then requires a cast. In reverse, use new ArrayList<>(Arrays.asList(array)) to get a mutable list from an array.

Can ArrayList store null values?

Yes. ArrayList allows any number of null entries. list.add(null) succeeds, list.contains(null) returns true, and list.indexOf(null) returns the index of the first null. However, calling methods on retrieved null elements causes NullPointerException. Collections that do not allow null — TreeSet, PriorityQueue, ArrayDeque — throw immediately on add. HashMap allows one null key and multiple null values.

What is the difference between List.of() and new ArrayList<>()?

List.of("A", "B", "C") returns a fixed-size, immutable Listadd(), remove(), and set() all throw UnsupportedOperationException. It is ideal for constants and read-only data. new ArrayList<>() returns a mutable, dynamically resizable list. To get a mutable copy from a List.of() result, wrap it: new ArrayList<>(List.of("A", "B", "C")). In interview contexts, be ready to explain the difference — it is a common question.

How do I make ArrayList thread-safe?

Two options: Collections.synchronizedList(new ArrayList<>()) wraps the list with a mutex that synchronises every individual method call. Iteration still requires external synchronisation: synchronized(syncList) { for (String s : syncList) ... }. The better option for read-heavy use is CopyOnWriteArrayList from java.util.concurrent — reads are lock-free, and writes copy the entire backing array. Use the synchronized wrapper when writes are frequent; use CopyOnWriteArrayList when writes are rare and concurrent iteration is common.

Why does ArrayList implement RandomAccess?

RandomAccess is a marker interface — it has no methods. Its presence on ArrayList signals to generic algorithms that index-based access is fast (O(1)). When Collections.sort() and Collections.binarySearch() receive a List, they check instanceof RandomAccess to decide whether to use index loops or iterator loops. LinkedList does not implement RandomAccess because its get(index) is O(n). This is a rare case where the JDK uses a marker interface to communicate performance characteristics to library algorithms.

What is trimToSize() and when should I call it?

ArrayList.trimToSize() reduces the backing array capacity to exactly size(), releasing any unused allocated memory. It is useful after a large bulk load that is now complete and will not grow further — for example, loading 50,000 product records from a database into a list that will be read but never modified again. The ArrayList may have capacity 65,536 after the load; trimToSize() releases the ~15,000 unused slots. In most code this is not necessary, but for long-lived large lists held in memory, it is worth the O(n) copy cost.

How does ArrayList.subList() work and what are its risks?

subList(int fromIndex, int toIndex) returns a List view of the specified range — it is not a copy. Modifications to the sublist are reflected in the original list and vice versa. The main risk is holding a reference to a sublist after the backing list has been structurally modified — subsequent sublist operations throw ConcurrentModificationException. The common safe pattern is to perform the operation and discard the sublist reference immediately: list.subList(0, 3).clear() removes the first three elements cleanly.

Summary

ArrayList is a resizable array backed by an Object[]. It grows by 50% when capacity is exhausted, gives O(1) random access by index, and costs O(n) for insertions or removals in the middle. The default choice for ordered sequences, return types from service methods, and any list that grows by appending. Pre-size when the count is known. Use removeIf() for conditional removal. Declare the variable as List<E>, never ArrayList<E>, so the implementation can be changed without rippling through the codebase.

For interviews: know the growth formula (oldCapacity + oldCapacity/2), know that add() is O(1) amortised and get() is O(1), understand why ConcurrentModificationException fires and what modCount tracks, and be able to explain why contains() on a large ArrayList should be replaced with a HashSet lookup.

What to Read Next