Java ArrayList Iterator: Usage and Common Pitfalls
java arraylist iterator: Learn how to use the Java ArrayList iterator, remove elements safely during iteration, and avoid ConcurrentModificationException.
When you call iterator() on a Java ArrayList, you get an Iterator<E> that walks the list from index 0 upward. The iterator is the mechanism behind the enhanced for-loop, but using it directly gives you capabilities the for-each syntax hides: safe element removal during traversal and bidirectional movement when you use ListIterator instead. The java arraylist iterator sits on top of the list's internal array and tracks a cursor position, which is why its behavior differs from a simple indexed loop in several important ways.
Obtaining the Iterator and Reading Elements
An ArrayList implements Iterable<E>, so you obtain an iterator through the iterator() method. The returned Iterator<E> exposes two methods you will use in every traversal: hasNext(), which reports whether another element exists, and next(), which returns the current element and advances the cursor.
List<String> names = new ArrayList<>(List.of("ada", "grace", "linus")); Iterator<String> it = names.iterator(); while (it.hasNext()) { String name = it.next(); System.out.println(name); }
The loop above is equivalent to the enhanced for-loop:
for (String name : names) { System.out.println(name); }
The compiler desugars the for-each form into an iterator-based loop for any Iterable. The difference is that the explicit version keeps the Iterator reference available, which matters when you need to remove an element or use ListIterator's extra methods.
Calling next() when hasNext() returns false throws NoSuchElementException. In practice, well-formed loops guard every next() call with hasNext(), so this exception usually appears only when the list is modified concurrently or when a next() call is made after the iterator has been exhausted.
Removing Elements Safely During Iteration
The most common mistake with the java arraylist iterator is attempting to remove an element by calling remove() on the list itself while iterating:
List<String> names = new ArrayList<>(List.of("ada", "grace", "linus")); for (String name : names) { if (name.equals("grace")) { names.remove(name); // throws ConcurrentModificationException } }
This throws ConcurrentModificationException because the enhanced for-loop holds an iterator internally, and the list's structural modification invalidates that iterator. The correct approach is to call remove() on the iterator:
Iterator<String> it = names.iterator(); while (it.hasNext()) { String name = it.next(); if (name.equals("grace")) { it.remove(); } }
Iterator.remove() removes the element most recently returned by next() and keeps the iterator valid. It is the only safe way to delete elements from an ArrayList while traversing it with an iterator. The List.remove(Object) method works fine outside a loop, but inside an active iterator it corrupts the iterator's internal state.
Why ConcurrentModificationException Happens
The exception is not caused by multithreading, despite its name. ArrayList maintains an internal modCount field that increments on every structural modification — any operation that adds or removes elements. When you create an iterator, it records the current modCount. On each call to next(), the iterator compares the recorded value with the list's current value. If they differ, it throws ConcurrentModificationException.
This is the fail-fast behavior of ArrayList's iterators. It is a defensive mechanism designed to surface bugs early rather than silently producing incorrect traversal results. The check is not a synchronization mechanism; it does not make the iterator thread-safe. If two threads modify the same ArrayList without external synchronization, the iterator may throw the exception, but you should not rely on that as a concurrency strategy.
Note that iterator.remove() does not trigger the exception because the iterator itself performs the removal and updates its recorded modCount accordingly. The same applies to ListIterator.remove() and ListIterator.add().
Iterator Performance and Memory Behavior
For an ArrayList, the iterator is backed by the same internal array and advances by incrementing an index. Each next() call is O(1): it performs a bounds check, reads the array element, and increments the cursor. There is no per-element allocation, so iterating an ArrayList with an iterator has the same asymptotic cost as an indexed for-loop.
The main allocation is the iterator object itself, created once per iterator() call. For most applications this is negligible, but in extremely hot loops that create a fresh iterator on every invocation, reusing the loop structure or using an indexed loop can avoid that small allocation. The difference is rarely measurable; choose the iterator when you need its removal semantics, and choose an indexed loop when you need the index value itself.
Memory usage is also flat: the iterator holds a reference to the list, a cursor position, and the recorded modCount. It does not copy the list or create a snapshot. This means the iterator reflects the current state of the list, which is precisely why structural changes invalidate it.
ListIterator for Bidirectional Traversal
ListIterator<E> extends Iterator<E> and adds methods for moving backward and modifying the list during traversal. You obtain it with listIterator() rather than iterator().
List<String> names = new ArrayList<>(List.of("ada", "grace", "linus")); ListIterator<String> li = names.listIterator(); while (li.hasNext()) { String name = li.next(); if (name.equals("grace")) { li.set("grace hopper"); } } while (li.hasPrevious()) { System.out.println(li.previous()); }
ListIterator adds previous(), hasPrevious(), set(E), and add(E). The set method replaces the element most recently returned by next() or previous() without invalidating the iterator. The add method inserts an element at the current cursor position. These operations are useful when you need to transform a list in place without building a second collection.
One subtlety: ListIterator indexes are tied to the cursor position, not to a fixed element. After calling next(), the cursor sits between the returned element and the following one. Methods like previousIndex() and nextIndex() report those positions, which can be useful when you need to know where an insertion will land.
Iterator vs Enhanced For-Loop vs Streams
The choice among these three approaches depends on what you need to do during traversal.
| Approach | Removal during loop | Access to index | Bidirectional | Best fit |
|---|---|---|---|---|
Iterator | Yes, via remove() | No | No | Safe removal, explicit control |
| Enhanced for-loop | No | No | No | Read-only traversal |
ListIterator | Yes | Indirect | Yes | In-place modification, reverse traversal |
| Indexed for-loop | Yes, with care | Yes | Yes | Need index, no iterator overhead |
| Stream | No (collect instead) | No | No | Filtering and mapping pipelines |
Use Iterator when you must remove elements while traversing. Use the enhanced for-loop for simple read-only iteration where the extra iterator variable would be noise. Use ListIterator when you need to move backward or modify elements in place. Use an indexed loop when you need the numeric index for something other than element access, such as pairing elements across two lists. Use a stream when you are filtering or transforming into a new collection and do not need to mutate the original.
The decision is driven by what the loop does, not by performance. All of these approaches traverse an ArrayList in linear time; the differences are in the operations available and the clarity of the resulting code.
Edge Cases and Structural Modification
An empty ArrayList returns an iterator whose hasNext() is immediately false, so a standard while-loop body never executes. That is expected behavior, not an error.
ArrayList permits null elements, and the iterator will return them like any other value. If your traversal logic calls methods on each element, guard against nulls explicitly when the list may contain them.
The most important edge case is structural modification after the iterator is created but before traversal completes. Any add, remove, or clear call on the list — from any code path — invalidates the iterator and triggers ConcurrentModificationException on the next next() call. This includes modifications made through a second iterator. If you need to collect elements for later removal, gather them first and remove them after the loop finishes:
List<String> names = new ArrayList<>(List.of("ada", "grace", "linus")); List<String> toRemove = new ArrayList<>(); for (String name : names) { if (name.startsWith("g")) { toRemove.add(name); } } names.removeAll(toRemove);
This two-pass approach avoids invalidating the iterator entirely and is often clearer than interleaving removal with traversal. It does create a temporary list, so it trades a small amount of memory for simpler logic. When the list is large and the removal condition is cheap, the iterator-based single pass is usually preferable.