Java List Remove: Methods, Pitfalls, and Performance
java list remove: Learn how to remove elements from a Java List using index, object, removeAll, and removeIf, and avoid common pitfalls like ConcurrentModificationExce...
When working with java.util.List, removing elements is a common operation, but the choice of removal method affects behavior, performance, and error handling. This article covers the main ways to perform a java list remove operation: by index, by object, in bulk, and during iteration, and explains when each is appropriate.
The Basic remove Methods
The List interface provides two straightforward removal methods. remove(int index) removes the element at the specified position and shifts any subsequent elements to the left. It returns the removed element. If the index is out of range, it throws IndexOutOfBoundsException. remove(Object o) removes the first occurrence of the specified element, using equals() to find it, and returns true if an element was removed. If the element is not present, the list remains unchanged and the method returns false.
List<String> list = new ArrayList<>(List.of("a", "b", "c", "b")); String removed = list.remove(1); // removes "b" at index 1, list becomes ["a", "c", "b"] boolean ok = list.remove("b"); // removes first "b" (now at index 2), list becomes ["a", "c"]
The remove(Object) method relies on equals(). For custom objects, ensure that equals() is overridden consistently with hashCode(); otherwise, you may not be able to remove elements as expected.
Removing Elements While Iterating
Modifying a list while iterating over it with a for-each loop or an enhanced for loop often leads to ConcurrentModificationException. The safe way to remove elements during iteration is to use the Iterator's own remove() method, which is designed for that purpose.
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5)); Iterator<Integer> iterator = numbers.iterator(); while (iterator.hasNext()) { if (iterator.next() % 2 == 0) { iterator.remove(); } } // numbers is now [1, 3, 5]
Java 8 introduced removeIf(Predicate<? super E> filter), which simplifies this pattern. It internally uses an iterator and removes all elements that satisfy the given predicate, so you do not need to manage the iteration manually.
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5)); numbers.removeIf(n -> n % 2 == 0); // removes even numbers, leaves [1, 3, 5]
removeIf is concise and avoids the risk of ConcurrentModificationException. It is the preferred approach when you need to remove elements based on a condition.
Removing Multiple Elements with removeAll and retainAll
When you need to remove a set of known elements, removeAll(Collection<?> c) removes every element in the list that is also present in the specified collection. Conversely, retainAll(Collection<?> c) keeps only the elements that are present in the specified collection and removes everything else. Both methods rely on equals() to determine membership.
List<String> list = new ArrayList<>(List.of("a", "b", "c", "d")); List<String> toRemove = List.of("b", "d"); list.removeAll(toRemove); // list becomes ["a", "c"]
A common performance trap is passing a List as the argument to removeAll. Because contains() on a List is linear, the overall operation becomes O(n * m), where n is the list size and m is the size of the collection to remove. Using a HashSet for the collection reduces the containment check to O(1), making the operation O(n) in practice.
Performance Considerations for List Removal
The cost of removal varies significantly between ArrayList and LinkedList. For ArrayList, remove(int index) is O(n) because it shifts all subsequent elements left. Removing the last element is O(1). remove(Object) is O(n) for the search and then O(n) for the shift. For LinkedList, remove(int index) is O(n) to traverse to the position, but the actual removal is O(1) once the node is found. However, LinkedList has higher constant overhead and worse cache locality.
If you frequently remove elements from the beginning of a list, LinkedList may seem appealing, but the traversal cost often negates the benefit. In practice, ArrayList is usually the better default unless you have a specific need for the Queue operations that LinkedList provides.
removeIf is efficient because it iterates through the list once and removes matching elements in place, without shifting elements multiple times. For large lists with many removals, this is often the fastest approach.
Common Pitfalls: ConcurrentModificationException and UnsupportedOperationException
Two exceptions frequently appear when removing elements from a list. The first is ConcurrentModificationException, which occurs when you modify a list structurally (adding or removing elements) while iterating over it with an iterator that does not support such modifications. The for-each loop uses an iterator internally, so calling list.remove() inside it throws this exception. Use iterator.remove() or removeIf instead.
The second is UnsupportedOperationException, which occurs when you try to modify a list that is immutable or fixed-size. For example, Arrays.asList() returns a fixed-size list backed by an array, so structural modifications like add or remove are not allowed. Similarly, List.of() returns an immutable list. Attempting to call remove on these throws UnsupportedOperationException.
List<String> fixed = Arrays.asList("a", "b"); fixed.remove(0); // throws UnsupportedOperationException
To avoid this, always create a mutable list with new ArrayList<>(...) when you need to modify it after creation.
Choosing the Right Removal Strategy
The right removal method depends on what you know about the list and what you want to achieve. Use remove(int index) when you have the exact position and need the removed element as a return value. Use remove(Object) to remove a single occurrence by value. Use removeIf when you need to remove elements that match a condition, especially if the condition is complex or the list is large. Use removeAll when you have a collection of elements to remove; for best performance, pass a HashSet if the collection is large. Use iterator.remove() when you are already iterating and need to remove the current element while also performing other operations during the iteration.
A practical approach is to prefer removeIf for most conditional removals, as it is both readable and efficient. For bulk removal of a known set, removeAll with a HashSet is straightforward. If you need to remove elements from the end of an ArrayList, remove(list.size() - 1) is O(1) and can be used in a loop without performance concerns.
Finally, be mindful of immutability. If you receive a list from List.of() or Arrays.asList(), you cannot remove elements directly. Copy it into a mutable ArrayList first if you need to modify it. This defensive copying is often necessary when dealing with APIs that return fixed-size or immutable collections.