Removing safely while iterating.
Deleting elements during a traversal is one of the most error-prone operations in Java — and the backward loop is the standard fix.
Removing elements from an ArrayList during a loop seems straightforward — but a naive forward loop skips elements every time it removes one. The result is silent data loss: matches survive when they shouldn't.
The fix is to iterate backwards. By starting at the last index and moving toward the front, each removal only shifts elements that have already been visited.
On the AP exam, safe-removal questions are some of the most common MCQ traps and a recurring FRQ requirement. Master the backward loop and you've defused half the ArrayList exam pitfalls.
Iterate backwards so removals don't shift the unvisited.
Start at size() - 1; decrement toward 0.
The unsafe forward loop
// BUGGY
for (int i = 0; i < list.size(); i++) {
if (shouldRemove(list.get(i))) {
list.remove(i);
}
}
Trace this on [0, 0, 1] looking for zeros:
- i=0:
list.get(0)is 0 → remove. List becomes[0, 1]. - i=1:
list.get(1)is 1 → keep. - Loop ends. Final:
[0, 1]. The second zero survived!
The safe backward loop
for (int i = list.size() - 1; i >= 0; i--) {
if (shouldRemove(list.get(i))) {
list.remove(i);
}
}
Trace on the same input [0, 0, 1]:
- i=2: value 1 → keep.
- i=1: value 0 → remove. List becomes
[0, 1]. - i=0: value 0 → remove. List becomes
[1].
All zeros gone. The backward loop visits each original index exactly once, regardless of how many removals happen.
Why backwards works
When you remove at index i, elements at positions i + 1, i + 2, ... shift left by one. In a backward loop, those positions have already been processed — the shifts touch only the visited region. The unvisited region (lower indices) is untouched.
Forward loop salvage (less clean)
If you must iterate forward, you can avoid the skip by not incrementing after a removal:
int i = 0;
while (i < list.size()) {
if (shouldRemove(list.get(i))) {
list.remove(i);
} else {
i++;
}
}
This works but is harder to read. AP-style code prefers the backward loop.
Safe-removal vocabulary.
Used in every removal-during-traversal question.
- Safe removal — deleting elements without skipping or crashing.
- Backward iteration — looping from
size() - 1down to0. - Index shift — what
remove(i)does to elements after positioni. - Skip bug — element after a removed one is missed by a forward loop.
- Visited region — indices already inspected by the loop.
- Unvisited region — indices the loop hasn't reached yet.
How safe removal is tested.
Identifying the bug and fixing it both appear.
What graders check:
- Backward iteration when removing. A forward loop with
remove(i)almost always has a skip bug. - Correct starting index.
size() - 1, notsize()(out of bounds). - Correct termination.
i >= 0, noti > 0(would miss index 0). - Decrement after every iteration.
i--in the loop header.
MCQ patterns: trace what survives after a buggy forward loop runs; identify which version of the code correctly removes every match.
Remove every negative value.
Compare forward (buggy) vs backward (safe).
Problem. Remove every negative value from [3, -1, -2, 5, -4, 7].
Forward (buggy) trace:
- i=0: 3, keep.
- i=1: -1, remove. List:
[3, -2, 5, -4, 7]. - i=2: 5, keep. (-2 was skipped!)
- i=3: -4, remove. List:
[3, -2, 5, 7]. - i=4: out of bounds — exit.
Wrong final: [3, -2, 5, 7]. -2 survived.
Backward (safe) trace:
- i=5: 7, keep.
- i=4: -4, remove. List:
[3, -1, -2, 5, 7]. - i=3: 5, keep.
- i=2: -2, remove. List:
[3, -1, 5, 7]. - i=1: -1, remove. List:
[3, 5, 7]. - i=0: 3, keep.
Correct final: [3, 5, 7]. Every negative gone.
Why this matters. Same input, same condition — only the loop direction differed. The backward loop is mechanical insurance against skip bugs.
Removal-loop pitfalls.
All produce silent data loss or runtime errors.
- Forward loop with
remove(i). Skips the element that takes the removed slot. - Starting at
size()instead ofsize() - 1. ThrowsArrayIndexOutOfBoundsExceptionon the first access. - Stopping at
i > 0instead ofi >= 0. Index 0 is never inspected. - Modifying
iinside the loop manually. Don't; the for loop's update handles it. - Trying to remove during an enhanced-for loop. Indeterminate behavior; just use an indexed loop.
- Caching
list.size()before the loop. The list shrinks; the cached value goes stale.
The safe-removal loop.
Memorize the three header components.
Safe Removal Reference
AP Quick Reference| Loop part | Correct value | Why |
|---|---|---|
| Initialization | i = list.size() - 1 |
Last valid index. |
| Condition | i >= 0 |
Includes index 0. |
| Update | i-- |
Move toward the front. |
| Body | Conditional remove(i) |
Shifts only visited region. |
| Direction | Backward | Prevents skip bug. |
How to make safe removal automatic.
If the prompt says "remove during a loop", reach for backward iteration without thinking.
Use the three practice tools below this lesson in order:
- MCQ Practice — for any code that calls
remove(i)inside a forward loop, suspect a skip bug and trace carefully. - Java Lab — implement remove-all-negatives, remove-all-zeros, remove-all-expired-products. Run them on inputs with consecutive matches to catch skip bugs.
- FRQ Practice — write the backward-loop header from memory:
for (int i = list.size() - 1; i >= 0; i--).
If you can write the safe-removal header without looking, the most common AP ArrayList trap is permanently solved.
Practice — attempt these now.
AP-style assessments aligned to this lesson. Time them.