Java
List Iteration
Removing Elements
Java Collections
Iterating Lists

Java How to remove elements from a list while iterating over/adding to it

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In Java, removing elements from a list while iterating can throw ConcurrentModificationException if done incorrectly. The safe approach depends on iteration style: use Iterator.remove(), collect and remove later, or use removeIf. Adding elements during iteration is similarly sensitive and may require separate accumulation.

Core Sections

1) Safe removal with iterator

java
1Iterator<String> it = list.iterator();
2while (it.hasNext()) {
3    String v = it.next();
4    if (shouldRemove(v)) {
5        it.remove();
6    }
7}

Iterator.remove() updates modification bookkeeping safely.

2) removeIf for concise filtering

java
list.removeIf(v -> shouldRemove(v));

This is often the cleanest modern Java approach.

3) Adding while iterating

Avoid adding to same list in enhanced for-loop. Use separate buffer.

java
1List<String> toAdd = new ArrayList<>();
2for (String v : list) {
3    if (shouldAddRelated(v)) {
4        toAdd.add(derive(v));
5    }
6}
7list.addAll(toAdd);

For index-based loops, changes to list size/index can still cause logic bugs.

4) Use ListIterator when needed

ListIterator supports safe add/remove during traversal with explicit cursor semantics.

java
1ListIterator<String> li = list.listIterator();
2while (li.hasNext()) {
3    String v = li.next();
4    if (condition(v)) li.add("new");
5}

Understand cursor behavior before using this in complex loops.

Verification Workflow and Operational Hardening

After implementing the fix, validate with a repeatable workflow rather than ad hoc manual checks. A reliable approach is: reproduce baseline, apply one focused change, then verify both expected behavior and nearby edge cases. This keeps debugging causal and makes reviews easier because every observed improvement is traceable to a specific diff.

A simple validation loop:

bash
1# 1) capture baseline output
2./run_case.sh > before.txt
3
4# 2) apply targeted fix from this article
5# edit code/config only in relevant area
6
7# 3) verify after-state and compare
8./run_case.sh > after.txt
9diff -u before.txt after.txt

For codebases with automated tests, immediately translate the reproduced issue into a regression test. This is the fastest way to prevent recurrence after refactors, dependency upgrades, or runtime migrations.

bash
1# typical quality gate sequence
2./lint.sh
3./test.sh
4./smoke.sh

Edge-case validation is essential. Many failures appear only on boundary inputs such as empty collections, null values, unusual encodings, large payloads, or high concurrency. Build a compact table of edge scenarios with expected outcomes, then run it in local and CI environments. This catches hidden assumptions early and reduces production surprises.

Environment parity also matters. A fix that works locally can fail elsewhere due to version differences, OS behavior, architecture (x86 vs ARM), filesystem semantics, or network policy. Capture runtime metadata alongside results so troubleshooting stays grounded in facts.

bash
1python --version
2node --version
3java -version
4git rev-parse --short HEAD

Before rollout, define rollback criteria and observability signals. Decide in advance which metrics/logs indicate success or regression, and document the rollback command path for on-call responders. Teams recover faster when fallback steps are predefined instead of improvised during incidents.

Finally, isolate functional fixes from broad refactors. Small, focused commits are easier to review, bisect, and revert safely. If normalization, formatting, or dependency upgrades are required, ship them in separate commits to keep risk controlled and diagnosis straightforward.

Common Pitfalls

  • Calling list.remove() inside enhanced for-loop.
  • Mixing index increments with list mutation and skipping elements.
  • Adding to same list during stream operations unexpectedly.
  • Assuming thread-safety where concurrent modification actually exists.
  • Using CopyOnWriteArrayList blindly for mutation-heavy workloads.

Summary

For Java list mutation during iteration, use iterator-aware APIs (Iterator.remove, ListIterator, removeIf) or two-phase add/remove strategies. Avoid direct structural changes in enhanced for-loops. Correct mutation patterns prevent ConcurrentModificationException and subtle logic errors.

A practical way to keep this solution robust over time is to add one focused regression test and one edge-case test that represent your real production data shape. Re-run those checks whenever dependencies, runtime versions, or infrastructure settings change. This small maintenance habit catches compatibility drift early and prevents recurring incidents that otherwise look like random regressions.


Course illustration
Course illustration