How to properly remove elements during iteration
The most common bug encountered by Java developers when modifying collections is invoking collection.remove() inside an enhanced for-each loop.
🟢 Junior Level
1. The Core Trap: Mutating While Iterating
The most common bug encountered by Java developers when modifying collections is invoking collection.remove() inside an enhanced for-each loop.
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie", "Bob"));
// ❌ ANTI-PATTERN: Throws ConcurrentModificationException!
for (String name : names) {
if ("Bob".equals(name)) {
names.remove(name); // Throws ConcurrentModificationException!
}
}
Why Does This Throw ConcurrentModificationException?
The enhanced for-each loop is syntactic sugar compiled down to an explicit Iterator:
// What the compiler actually generates:
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next(); // Checks: if (modCount != expectedModCount) throw CME
if ("Bob".equals(name)) {
names.remove(name); // Modifies list: modCount++ (expectedModCount remains untouched)
}
}
When names.remove() executes, the list increments its internal structural modification counter (modCount++). On the subsequent iteration, it.next() compares its cached expectedModCount against names.modCount. Because they no longer match, it immediately throws ConcurrentModificationException (CME).
2. The Two Production-Grade Solutions
Approach 1: Collection.removeIf() (Modern Java 8+ Standard)
The cleanest, fastest, and most expressive approach:
// ✅ RECOMMENDED: In-place, linear-time filter
names.removeIf(name -> "Bob".equals(name));
Approach 2: Explicit Iterator.remove() (Legacy & Imperative Control)
Use this approach when you need side effects, step-by-step logging, or need to pass the iterator across helper methods:
// ✅ SAFE CLASSICAL APPROACH:
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
if ("Bob".equals(name)) {
it.remove(); // Safely removes current element and synchronizes expectedModCount!
}
}
[!NOTE]
Iterator.remove()is safe because it synchronizes the iterator state: after callinglist.remove(lastRet), the iterator updates its internal fieldexpectedModCount = modCount.
🟡 Middle Level
1. Removal Strategies Comparison Matrix
| Strategy | ArrayList Time |
LinkedList Time |
Space Overhead | Thread-Safe? | Avoids CME? | Primary Use Case |
|---|---|---|---|---|---|---|
Collection.removeIf() |
$O(N)$ | $O(N)$ | $O(1)$ / BitSet | ❌ No | ✅ Yes | Standard production code |
Iterator.remove() |
$O(N^2)$ | $O(N)$ | $O(1)$ | ❌ No | ✅ Yes | Complex multi-step traversal logic |
Reverse Indexed for (i--) |
$O(N^2)$ | $O(N^2)$ | $O(1)$ | ❌ No | ✅ Yes | Interview quiz; primitive array loops |
Stream.filter().toList() |
$O(N)$ | $O(N)$ | $O(N)$ | ❌ No | ✅ Yes | Functional pipelines, immutability |
Temporary removeAll() |
$O(N^2)$ | $O(N \cdot M)$ | $O(M)$ | ❌ No | ✅ Yes | Anti-pattern; avoid in large datasets |
2. The $O(N^2)$ Degradation of Iterator.remove() on ArrayList
While Iterator.remove() prevents ConcurrentModificationException, executing it repeatedly on an ArrayList causes an asymptotic performance collapse:
ArrayList Memory Layout during Iterator.remove():
Index: [ 0 ] [ 1 ] [ 2 ] [ 3 ] ... [ 999,998 ] [ 999,999 ]
Iteration: Remove item at index 0
Action: System.arraycopy(elementData, 1, elementData, 0, 999,999) -> shifts 999,999 elements!
Iteration: Remove item at next match...
Action: System.arraycopy(...) shifts remaining elements again!
- Each call to
ArrayList.Itr.remove()delegates toArrayList.this.remove(lastRet). ArrayList.remove(index)callsSystem.arraycopy()to shift all subsequent elements one slot to the left.- If you have $N = 1{,}000{,}000$ elements and remove $500{,}000$ items: \(\text{Total Copies} \approx \sum_{k=1}^{500{,}000} (1{,}000{,}000 - k) \approx 375{,}000{,}000{,}000 \text{ references moved}\)
- Runtime Impact: This takes tens of seconds to minutes, thrashing CPU L1/L2 caches and memory buses.
Contrast with LinkedList
In LinkedList, Iterator.remove() executes in strict $O(1)$ per removal:
// LinkedList.Node pointer rewiring:
Node<E> next = node.next;
Node<E> prev = node.prev;
if (prev == null) first = next; else prev.next = next;
if (next == null) last = prev; else next.prev = prev;
Because the iterator maintains direct references to the current node, unlinking requires no element shifts. Total removal time for $N$ items in LinkedList is strictly $O(N)$.
3. Loop Pitfalls: Forward vs Reverse Indexing
Forward Loop Index Skipping Trap
// ❌ CRITICAL BUG: Skips consecutive matching elements!
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals("Bob")) {
list.remove(i); // Elements shift left; index i now points to the NEXT element!
// Next loop iteration increments i++, skipping that element!
}
}
If "Bob" appears at index 2 and index 3, removing index 2 causes index 3 to shift down to index 2. The loop then increments i to 3, completely skipping evaluation of the second "Bob".
Reverse Indexed Loop Fallacy
// ⚠️ AVOIDS CME & SKIPPING, BUT ASYMPTOTICALLY SLOW:
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i).equals("Bob")) {
list.remove(i);
}
}
- Why it avoids CME: The loop uses raw index operations (
list.get(i),list.remove(i)) without creating anIteratorinstance. AlthoughmodCountincrements, no iterator exists to check it. - Why it avoids skipping: Removing an element only shifts elements to its right. Because reverse iteration moves to the left (
i--), unprocessed elements are unaffected. - Why it is an anti-pattern:
- On
ArrayList: Removing elements near the beginning still incurs massive $O(N)$ memory copies viaSystem.arraycopy(), resulting in overall $O(N^2)$ time. - On
LinkedList:list.get(i)traverses from the nearest end on every iteration, turning the loop into an $O(N^2)$ disaster even before removals are considered!
- On
4. The Temporary Collection Trap (list.removeAll(toRemove))
// ❌ POOR PATTERN:
List<String> toRemove = new ArrayList<>();
for (String s : list) {
if (predicate(s)) toRemove.add(s);
}
list.removeAll(toRemove);
- Heap Allocation: Allocates an unnecessary intermediate
ArrayList, increasing GC pressure. - Algorithmic Collapse:
ArrayList.removeAll(Collection<?> c)iterates over its own elements and invokesc.contains(e)for every element. IftoRemoveis aList,contains()performs an $O(M)$ linear scan: \(\text{Complexity} = O(N \times M) \approx O(N^2)\) - If
toRemoveis converted to aHashSet, lookups drop to $O(1)$, but at the expense of heavy object allocation (hashing nodes, wrappers, array table).
🔴 Senior Level
1. Deep Dive: ArrayList.removeIf() Under the Hood
In OpenJDK, ArrayList overrides Collection.removeIf() with a highly optimized two-phase compaction algorithm that executes in strictly $O(N)$ time with zero memory copies unless modifications are necessary.
// OpenJDK ArrayList.java implementation:
@Override
public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
int removeCount = 0;
final BitSet removeSet = new BitSet(size);
final int expectedModCount = modCount;
final int size = this.size;
// Phase 1: Scan and record matching indices in a compact BitSet
for (int i = 0; modCount == expectedModCount && i < size; i++) {
@SuppressWarnings("unchecked")
final E element = (E) elementData[i];
if (filter.test(element)) {
removeSet.set(i);
removeCount++;
}
}
// Fail-fast detection during predicate evaluation
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
final boolean anyToRemove = removeCount > 0;
if (anyToRemove) {
final int newSize = size - removeCount;
// Phase 2: In-place two-pointer compaction
for (int i = 0, j = 0; (i < size) && (j < newSize); i++, j++) {
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
// Phase 3: Prevent memory loitering (null out tail references for GC)
for (int k = newSize; k < size; k++) {
elementData[k] = null;
}
this.size = newSize;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
return anyToRemove;
}
Architectural Key Points:
- Zero-Write Optimization: If
removeCount == 0, the method terminates without performing a single write toelementData. - Predicate Tamper Detection: If
filter.test(element)modifies the list concurrently or reentrantly,modCount != expectedModCounttriggers an immediate CME before corrupting data structures. - BitSet Compaction: Uses
removeSet.nextClearBit(i)to jump directly to surviving elements, copying each surviving reference exactly once:elementData[j] = elementData[i]. - Memory Loitering Prevention:
for (int k = newSize; k < size; k++) { elementData[k] = null; // Let GC reclaim orphaned objects! }If elements were merely shifted and
sizedecremented, slots betweennewSizeand oldsizewould retain strong references to deleted objects in heap memory. This would cause a silent memory leak known as memory loitering. - Single Structural Update:
modCount++is executed exactly once at the end of the batch modification, rather than $K$ times.
2. Removals in Concurrent Collections
| Concurrent Collection | Removal Mechanism | Behavior During Concurrent Iteration |
|---|---|---|
CopyOnWriteArrayList |
Snapshot Array | iterator().remove() throws UnsupportedOperationException. removeIf() acquires the internal synchronization lock and atomically swaps the backing array. |
ConcurrentHashMap |
CAS + Striped Bins | it.remove() is fully thread-safe and weakly consistent. Calls ConcurrentHashMap.this.remove(key, value) without throwing CME. |
ConcurrentLinkedQueue |
Lock-Free CAS Unlinking | it.remove() safely unlinks nodes using CAS operations (item.compareAndSet(x, null)), leaving phantom unlinked nodes for background GC. |
Collections.synchronizedList() |
Mutex Wrapper | Client must manually synchronize the entire iteration block on the list mutex; otherwise, external removals cause CME. |
// Manual synchronization required for synchronized collections:
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
synchronized (syncList) {
Iterator<String> it = syncList.iterator();
while (it.hasNext()) {
if ("target".equals(it.next())) {
it.remove(); // Must be inside synchronized block!
}
}
}
4 Tricky Questions
1. Why does a reverse for-i loop avoid ConcurrentModificationException, and when does it become an antipattern?
Answer:
A reverse index loop for (int i = list.size() - 1; i >= 0; i--) operates directly on list indices via list.get(i) and list.remove(i). It does not create an Iterator instance on the heap. While list.remove(i) increments the internal modCount, there is no iterator checking expectedModCount == modCount, so no CME is ever thrown.
It becomes an antipattern in two major scenarios:
- On
LinkedList: Callinglist.get(i)andlist.remove(i)requires traversing linked nodes from the ends on every iteration. This degrades overall complexity to $O(N^2)$. - On large
ArrayLists: Removing items from the first half of the list still invokesSystem.arraycopy()to shift surviving elements to the left, degrading runtime to $O(N^2)$.Collection.removeIf()should always be preferred.
2. What happens if you call collection.remove() inside a for-each loop but immediately break?
for (String s : list) {
if ("target".equals(s)) {
list.remove(s);
break; // Does this throw CME?
}
}
Answer:
It will not throw ConcurrentModificationException.
The enhanced for-each loop checks modCount inside it.hasNext() and it.next(). When list.remove(s) executes, modCount is incremented. However, because break immediately jumps execution out of the loop block, neither it.hasNext() nor it.next() is ever called again.
Why this is dangerous in production:
- It only works if you intend to remove at most one element.
- If a future developer removes the
breakor wraps the removal in additional logic, the code immediately crashes with CME. - If the collection contains multiple matches, only the first is removed. Use
list.remove(item)directly without a loop if you only want to remove the first match!
3. What algorithmic catastrophe occurs when calling list.removeAll(toRemove) where toRemove is a List?
Answer:
The default implementation in AbstractCollection.removeAll(Collection<?> c) iterates over the calling collection and tests membership:
public boolean removeAll(Collection<?> c) {
boolean modified = false;
Iterator<?> it = iterator();
while (it.hasNext()) {
if (c.contains(it.next())) { // Linear search if c is an ArrayList!
it.remove();
modified = true;
}
}
return modified;
}
If c (toRemove) is an ArrayList, c.contains(e) performs an $O(M)$ linear scan through toRemove. If the host list has $N$ elements and toRemove has $M$ elements, total comparisons equal $O(N \times M)$. On an ArrayList, each it.remove() also incurs an $O(N)$ shift. In the worst case, this leads to an catastrophic $O(N^2 \times M)$ or $O(N \times M)$ operation that can lock up a production thread.
Remedy: If removeAll() must be used, convert toRemove to a HashSet first ($O(1)$ lookup time) or use list.removeIf(toRemoveSet::contains).
4. What is “memory loitering”, and how does ArrayList.removeIf() prevent it?
Answer: Memory loitering occurs when an internal data structure retains an object reference that the application logic has already discarded, preventing the Garbage Collector from freeing the allocated memory.
In ArrayList, the capacity of the internal elementData[] buffer is frequently larger than size. When $K$ elements are removed and surviving elements are shifted forward, the slots from index newSize to oldSize - 1 would continue pointing to the old objects if left untouched. Even though list.get(index) rejects access beyond newSize, the JVM GC root analysis sees active references in elementData[k], preventing GC reclamation.
ArrayList.removeIf() explicitly prevents this by executing:
for (int k = newSize; k < size; k++) {
elementData[k] = null; // Explicitly clears reference for Garbage Collector
}
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“To remove elements during iteration in Java, the golden standard is
Collection.removeIf(Predicate)introduced in Java 8. InArrayList, it runs in $O(N)$ linear time using a two-pointer BitSet compaction algorithm and explicitly nulls out tail references to prevent memory loitering. The legacy safe way isIterator.remove(), which updatesexpectedModCountto preventConcurrentModificationException, but degrades to $O(N^2)$ onArrayListdue to repeatedSystem.arraycopy()shifts. Never callcollection.remove()inside an enhancedfor-eachloop because the iterator detects structural modifications and throwsConcurrentModificationException.”
Red Flags & Pitfalls (DO NOT Say)
- ❌ “Calling
list.remove()inside afor-eachloop is fine as long as you wrap it in a try-catch.” (Catching CME to control flow is atrocious design and corrupts iterator state). - ❌ “A reverse index loop is just as fast as
removeIf().” (Reverse loops still trigger $O(N^2)$ memory copying onArrayListand $O(N^2)$ traversal onLinkedList). - ❌ “
Iterator.remove()works onCopyOnWriteArrayList.” (It throwsUnsupportedOperationExceptionbecause COW iterators operate on immutable snapshots). - ❌ “
Stream.filter()modifies the list in place.” (Stream.filter()creates a new pipeline and requires.toList()or.collect(), allocating a brand-new collection).
Summary Checklist
Removal Technique Decision Tree:
├─ Need to mutate existing collection in place?
│ ├─ Modern Java 8+? ──────────────> Collection.removeIf(predicate) [O(N), Best]
│ ├─ Complex imperative steps? ────> Iterator.remove() [O(N) LinkedList, O(N^2) ArrayList]
│ └─ Concurrent multithreaded? ────> ConcurrentHashMap.keySet().removeIf() or COWAL.removeIf()
└─ Prefer immutable functional flow? ─> list.stream().filter(...).toList() [Allocates new list]