Java ArrayList remove: Methods, Behavior, and Pitfalls
java arraylist remove: Learn how to remove elements from a Java ArrayList using index-based and object-based removal, bulk operations, and safe iteration. Understand t...
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 java 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
Note that when you call remove with a primitive int, Java automatically boxes it to Integer only if you pass an Integer object. The overload resolution is based on the compile-time type. If you have a list of Integer values and want to remove the value 5 (not the element at index 5), you must pass an Integer explicitly:
List<Integer> numbers = new ArrayList<>(List.of(10, 5, 20)); numbers.remove(1); // removes the element at index 1 (the value 5) numbers.remove(Integer.valueOf(5)); // removes the first occurrence of 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
After removal, 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).
Set<Integer> toRemoveSet = new HashSet<>(List.of(2, 4)); numbers.removeAll(toRemoveSet);
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, and if you call list.remove() during iteration, the iterator's expected modification count no longer matches the list's actual modification count, throwing a ConcurrentModificationException.
List<String> list = new ArrayList<>(List.of("a", "b", "c")); for (String s : list) { if (s.equals("b")) { list.remove(s); // 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 remove is the shifting of elements. If your application frequently removes elements from the beginning or middle of a large list, consider using a LinkedList, which supports O(1) removal at either end, but has worse random access. However, LinkedList has its own overhead with node objects and is often slower in practice for small lists. A better approach for many removals 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.