Back to Blog
Java

Java ArrayList remove: Methods, Behavior, and Pitfalls

Learn how to remove elements from a Java ArrayList using index-based and object-based removal, bulk operations, safe iteration, and common pitfalls.

ArrayListJava Collectionsremove methoditerationperformance
Illustration of removing an element from a Java ArrayList, showing elements shifting left to fill the gap.

Removing elements from a Java ArrayList is a routine operation, but the API offers several methods with different semantics and runtime costs. Choosing the wrong removal strategy can lead to ConcurrentModificationException, index errors, or unnecessary copying. This article explains how ArrayList.remove() works in practice, covering the core methods, their behavior under the hood, and the tradeoffs you should consider when removing elements in real applications.

The Core Removal Methods: remove(int index) and remove(Object o)

The ArrayList class provides two overloaded remove methods. The first takes an int and removes the element at that position, returning the removed element. The second takes an Object and removes the first occurrence of an element that equals the given object, returning a boolean to indicate whether anything was removed.

List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie")); String removedByName = names.remove(1); // removes "Bob", returns "Bob" boolean removedByObject = names.remove("Alice"); // removes "Alice", returns true

These examples use List.of, which is available from Java 9 onward; on Java 8, use Arrays.asList instead.

Overload resolution is based on the compile-time type of the argument. If you pass an int, the compiler selects remove(int) and removes by index. If you pass an Integer, it selects remove(Object) and removes by value. This distinction matters for List<Integer>: to remove the value 5 rather than the element at index 5, pass an Integer:

List<Integer> byIndex = new ArrayList<>(List.of(10, 5, 20)); byIndex.remove(1); // removes the element at index 1 (the value 5) List<Integer> byValue = new ArrayList<>(List.of(10, 5, 20)); byValue.remove(Integer.valueOf(5)); // removes the first occurrence of the value 5

This distinction is a common source of bugs. Always verify whether you are removing by index or by value.

How remove(int index) Works Under the Hood

When you remove an element by index, the ArrayList must shift all subsequent elements one position to the left to fill the gap. This is a linear-time operation: the worst-case cost is O(n) where n is the number of elements after the removed position. Removing the last element is O(1) because no shifting is needed; removing the first element is O(n) because every remaining element moves.

The implementation internally uses System.arraycopy to move the elements, which is a native call and very fast for small arrays, but the asymptotic cost remains linear. If you remove many elements from the beginning of a large list, the total cost can become quadratic. Consider this when designing algorithms that repeatedly remove the first element.

List<String> list = new ArrayList<>(List.of("a", "b", "c", "d")); list.remove(0); // shifts "b", "c", "d" left

In the standard implementation, the last element in the backing array is set to null to help garbage collection, and the size is decremented.

Removing by Object Reference: equals() and Null Handling

The remove(Object o) method scans the list from the beginning and removes the first element for which o.equals(element) returns true. If the list contains null, you can remove it by passing null as the argument, because null.equals(element) is never called; the implementation checks o == null ? element == null : o.equals(element).

List<String> list = new ArrayList<>(); list.add(null); list.add("value"); boolean removed = list.remove(null); // true, removes the null element

If the object is not found, the list remains unchanged and the method returns false. This method also runs in O(n) time because it must scan the list until it finds a match. If you need to remove all occurrences of an element, remove(Object) only removes the first match. To remove all occurrences, you can use a loop or removeAll(Collections.singleton(o)).

Removing Multiple Elements: removeAll and retainAll

ArrayList provides bulk removal operations. removeAll(Collection<?> c) removes every element that is contained in the specified collection. retainAll(Collection<?> c) keeps only the elements that are contained in the specified collection and removes everything else.

List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5)); List<Integer> toRemove = List.of(2, 4); numbers.removeAll(toRemove); // numbers becomes [1, 3, 5] List<Integer> toKeep = List.of(1, 5); numbers.retainAll(toKeep); // numbers becomes [1, 5]

These methods iterate over the list and use contains on the provided collection to decide whether to keep or remove each element. The runtime cost is O(n * m) in the worst case, where n is the size of the list and m is the size of the collection, unless the collection has a fast contains implementation (like a HashSet). If you need to remove many elements, consider passing a HashSet to removeAll to reduce the lookup cost to O(1) per element, making the overall operation O(n).

List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5)); Set<Integer> toRemoveSet = new HashSet<>(List.of(2, 4)); numbers.removeAll(toRemoveSet); // numbers becomes [1, 3, 5]

Another way to clear the entire list is clear(), which removes all elements and sets the size to zero. This is O(n) because the backing array is filled with null references, but it does not shrink the array capacity. If you want to free memory, you can assign a new ArrayList or call trimToSize() after clearing.

Safe Removal During Iteration: Iterator and ListIterator

Removing elements while iterating over an ArrayList using the for-each loop or a traditional for loop with an index can produce unexpected behavior. The for-each loop uses an Iterator internally. If you call list.remove() during iteration and there are remaining elements, the list's modification count changes, and the next call to next() detects the mismatch and throws a ConcurrentModificationException.

List<String> list = new ArrayList<>(List.of("a", "b", "c", "d")); for (String s : list) { if (s.equals("b")) { list.remove(s); // the next call to next() throws ConcurrentModificationException } }

To remove elements safely while iterating, use the Iterator's own remove() method. The iterator knows about the modification and updates its internal state accordingly.

Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String s = iterator.next(); if (s.equals("b")) { iterator.remove(); } }

If you need to remove elements at specific indices while iterating, you can use ListIterator and iterate backwards, or use a for loop that decrements the index after removal to avoid skipping elements.

for (int i = list.size() - 1; i >= 0; i--) { if (list.get(i).equals("b")) { list.remove(i); } }

This backward iteration avoids index shifting issues because removing an element at a lower index does not affect already visited indices.

Performance Considerations and When to Use Alternatives

The primary performance cost of ArrayList.remove is shifting subsequent elements. If your application repeatedly removes the first or last element, a LinkedList can make those endpoint removals O(1), but it has worse random access and per-node overhead. A LinkedList does not make removal by value or by arbitrary index O(1); those operations still require traversal. For many removals, a better approach is to collect the elements you want to keep into a new list, or use a stream with a filter.

List<String> filtered = list.stream() .filter(s -> !s.equals("b")) .collect(Collectors.toList());

This creates a new list, so it avoids mutating the original. If you must mutate the original list and you need to remove many elements, using removeAll with a HashSet is often the most efficient approach because it avoids repeated shifting.

Another consideration is memory. ArrayList does not shrink its backing array when elements are removed. If you remove many elements and the list remains large in capacity, you may want to call trimToSize() to release unused memory, especially if the list is long-lived.

Common Pitfalls and Edge Cases

One common mistake is using remove on a sublist view. The subList method returns a view of the original list, and modifications to the sublist are reflected in the parent list. However, if you structurally modify the parent list (e.g., by adding or removing elements) after creating a sublist, the sublist becomes invalid and any subsequent operation throws ConcurrentModificationException.

List<String> list = new ArrayList<>(List.of("a", "b", "c", "d")); List<String> sub = list.subList(1, 3); // ["b", "c"] list.add("e"); // structural modification sub.remove(0); // throws ConcurrentModificationException

Another edge case is removing an element by index that is out of bounds. This throws IndexOutOfBoundsException. Always check the current size before calling remove(int index) if the index comes from external input.

Finally, remember that remove(Object) relies on the equals implementation of the elements. If your class does not override equals, the default reference equality is used, which may not match your intent. For value-based removal, ensure equals is properly implemented.

Understanding these behaviors and costs helps you choose the right removal strategy for your specific scenario, avoiding performance traps and runtime exceptions.

java arraylist remove: Practical Usage and Code Examples | RYUSLOG DEV