1. Introduction to Iterator and ListIterator
Whenever a Java program uses an enhanced for loop to visit each element of a collection, an iterator is working behind the scenes. An iterator provides a controlled way to move through a collection, retrieve elements, and—in supported collections—remove or update elements during traversal.
For example, an application might walk through a shopping cart to display products, remove line items whose quantities are zero, or move backward through a customer's navigation history. These tasks are easier to understand when the cursor model of an iterator is clear.
Java provides two closely related interfaces:
Iterable<T>indicates that an object can provide an iterator and therefore can be used in an enhancedforloop.Iterator<T>represents the cursor that actually traverses elements.ListIterator<E>extends the basic traversal model for lists by adding backward movement, index reporting, replacement, and insertion.
The central distinction is simple: Iterable supplies an iterator; the iterator performs the traversal.
2. Iterable vs Iterator
2.1 The Iterable<T> interface
Iterable<T> represents an object whose elements can be traversed. Its key method is:
iterator()— returns an iterator for traversing the elements.
Standard collection types such as ArrayList, HashSet, and LinkedList implement Iterable, which is why their instances can be used in enhanced for loops.
Calling iterator() generally gives the caller a new traversal cursor. The cursor has its own current position, separate from the collection's storage.
2.2 The Iterator<T> interface
Iterator<T> is the cursor. It provides three fundamental methods:
hasNext()checks whether another element is available.next()returns the next element and advances the cursor.remove()removes the element most recently returned bynext(), if the iterator supports removal.
The iterator does not need to expose how the collection stores its data. It presents a consistent traversal interface over different underlying data structures.
2.3 How the enhanced for loop works
An enhanced for loop over an Iterable is shorthand for obtaining an iterator and repeatedly checking hasNext() before calling next(). Conceptually, these two styles perform the same traversal:
The equivalent iterator-based pattern is:
The enhanced for form is more concise. The explicit iterator form is useful when the program needs to remove the current element safely or control traversal more directly.
2.4 The cursor sits between elements
Think of an iterator as an arrow positioned between collection elements, rather than directly on an element. Initially, it sits before the first element. Each next() call returns the element to the right of the cursor and moves the cursor past that element.
hasNext() asks whether an element remains to the right. It checks availability without advancing the cursor.
3. The Three Main Iterator Methods
3.1 hasNext()
hasNext() returns true if a call to next() can return another element. Otherwise, it returns false.
It does not move the cursor. It can be called several times before next() without consuming the next element, provided the iterator's state has not changed.
3.2 next()
next() returns the next element and advances the cursor. If no elements remain, it throws NoSuchElementException.
A reliable traversal pattern checks hasNext() before calling next():
Calling next() repeatedly without checking whether another element exists can cause an exception when the cursor reaches the end. This is why the two methods commonly appear together.
3.3 remove()
Unlike many removal methods, Iterator.remove() does not accept an element or index. It removes the element most recently returned by next().
The typical sequence is:
- Call
next()to retrieve an element. - Decide whether that element should remain.
- If it should be removed, call
remove()on the same iterator.
Two rules are especially important:
- An element must first be returned by
next()beforeremove()can be called. remove()can be called only once for each successfulnext()call.
Violating either rule results in IllegalStateException. Some iterators do not support removal and may throw UnsupportedOperationException.
4. Safely Removing Elements During Iteration
A common collection task is removing entries that no longer meet a business rule. For example, an order may contain line items whose quantity is zero. Removing those entries while iterating with an ordinary enhanced for loop is unsafe because the loop's hidden iterator is not available for direct removal.
Use an explicit iterator when removal needs to happen as part of the traversal:
The iterator removes the element it just returned and adjusts its own traversal state. The loop can continue without accidentally skipping the next element due to an index shift.
4.1 Why direct collection removal can fail
Calling items.remove(item) while a separate iterator is traversing items can structurally modify the collection outside the iterator's control. For many standard collections, the next iterator operation detects the change and throws ConcurrentModificationException.
The issue is not simply that removal is always forbidden during traversal. Rather, the modification must follow the collection iterator's supported protocol. Iterator.remove() is the standard safe option for removing the last-returned element during iteration.
4.2 Performance considerations
The cost of removal depends on the underlying data structure:
- Removing from an
ArrayListthrough its iterator can take O(n) time per removal because later array elements may need to shift left. - Removing from a
LinkedListthrough its iterator is typically O(1) once the iterator has reached the relevant node, because it can unlink that node directly.
Consequently, removing many elements from a large array-backed list can require substantial shifting work.
4.3 Use removeIf() for simple conditions
Java 8 introduced Collection.removeIf(Predicate), which removes every element matching a condition. It is often the clearest option when the task is simply to delete all items that satisfy a predicate.
Prefer removeIf() for a straightforward “remove everything matching this rule” operation. Prefer an explicit iterator when you need more control, such as stopping after the first match, removing selectively, or performing additional work during traversal.
5. Fail-Fast Iterators and ConcurrentModificationException
5.1 What is a structural modification?
A structural modification changes the collection's structure, usually by changing its size or otherwise changing its element arrangement. Examples include adding, removing, or clearing elements.
Replacing an existing value without changing the collection's size is generally not a structural modification. For example, replacing the object at an existing list index does not normally cause the same fail-fast response as adding or removing an element.
5.2 Why the exception occurs
Many standard non-concurrent collections, including ArrayList, HashMap, and HashSet, use an internal modification counter commonly known as modCount. An iterator records the collection's modification state when it is created. During traversal, it can compare its expected state with the collection's current state.
If the collection has been structurally modified through a path the iterator does not control, the iterator may throw ConcurrentModificationException. Exact checking points vary by implementation; the exception is not guaranteed to appear at every possible point of an invalid modification.
5.3 The meaning of fail-fast
Fail-fast behavior is intended to expose programming mistakes early. Without detection, external structural changes could cause traversal to skip elements, revisit elements, or otherwise produce confusing results.
The name can be misleading: fail-fast does not mean thread-safe. It is a best-effort bug-detection mechanism, not a synchronization guarantee. Two threads modifying the same ordinary collection may or may not trigger the exception, and an exception does not undo any changes already made.
5.4 Choosing concurrent collections
When multiple threads need to access and modify shared data, choose a synchronization strategy or an appropriate concurrent collection rather than relying on fail-fast checks. Concurrent collections often provide weakly consistent iterators: traversal does not throw ConcurrentModificationException merely because concurrent updates occur, but the iterator is not necessarily a snapshot of all changes made after it was created.
The right choice depends on whether the application needs a stable snapshot, visibility of some concurrent changes, high-throughput concurrent access, or external synchronization.
5.5 Practical remedies
When a collection must be changed during traversal:
- Use the iterator's own
remove()method to remove the last-returned element. - Use
removeIf()when all elements matching a simple predicate should be removed. - Consider a concurrent collection or explicit synchronization for genuinely concurrent access.
- Avoid assuming that fail-fast behavior makes unsafe modification reliable.
- Loading diagram...
6. Introducing
ListIterator
ListIterator<E> is a specialized iterator available for implementations of List. It supports the ordinary iterator capabilities and adds bidirectional traversal, index reporting, element replacement, and insertion at the cursor's current position.
Create one with either:
list.listIterator()— starts at the beginning of the list.list.listIterator(index)— starts at the specified position. The next call tonext()returns the element at that index.
The cursor remains positioned between elements, but unlike a basic iterator it can move both forward and backward.
7. ListIterator Methods Explained
| Method | Purpose |
|---|---|
hasNext() | Checks whether an element exists to the right of the cursor. |
next() | Returns the element to the right and advances the cursor. |
hasPrevious() | Checks whether an element exists to the left of the cursor. |
previous() | Returns the element to the left and moves the cursor backward. |
nextIndex() | Returns the index that a subsequent next() would return; at the end, this equals the list size. |
previousIndex() | Returns the index that a subsequent previous() would return; at the beginning, this is -1. |
remove() | Removes the element most recently returned by next() or previous(), when supported. |
set(E e) | Replaces the element most recently returned by next() or previous(), when supported. |
add(E e) | Inserts an element at the cursor's current position, when supported. |
The same last-returned-element rule matters for remove() and set(): the iterator needs a valid element returned by a traversal method before either operation can act on it. Unsupported operations can throw UnsupportedOperationException.
8. Forward and Backward Traversal
A useful example is a browser-like navigation history containing Home, Products, Cart, and Checkout. The program can first walk forward through the history and then walk backward using the same ListIterator.
After the forward loop finishes, the cursor is at the end of the list. The first previous() call returns the last element, then moves the cursor one position toward the beginning. The backward loop ends when hasPrevious() returns false.
A small but important detail is that calling next() and then previous() returns the same element twice in succession, while restoring the cursor to its original position. next() moves across an element; previous() moves back across that same element.
9. Understanding ListIterator Indexes
The index methods describe the elements that would be returned by the next traversal operation, rather than simply reporting a fixed “current element index.”
- At the beginning of a list,
nextIndex()is0andpreviousIndex()is-1. - At the end of a list of size
n,nextIndex()isnandpreviousIndex()isn - 1.
This model is useful for inserting elements at a particular position, displaying positional information, or implementing a list editor. It also helps explain why the cursor is best imagined as a boundary between elements.
10. Replacing and Inserting Elements with ListIterator
10.1 Replacing the last-returned element with set()
set(E e) replaces the element most recently returned by next() or previous(). It does not change the list's size, so it is not a structural size change.
For example, after next() returns "Pen", calling set("Gel Pen") replaces that returned element in place. The cursor stays at its current boundary.
10.2 Inserting at the cursor with add()
add(E e) inserts a new element at the cursor's current position. Insertion changes the list's size, but because the operation is performed through the list iterator, the iterator can update its own state and continue traversal.
If the cursor is just after "Pen", inserting "Pen Refill" places the new item at that cursor position. Elements after the insertion shift to later indexes in an array-backed list. The cursor then sits after the inserted element, so the next forward traversal proceeds to the element that originally followed the insertion point.
10.3 The operation-order rule
After add(), a call to set() or remove() is not allowed until another successful next() or previous() call establishes a new last-returned element. The insertion itself does not count as an element returned by traversal. Likewise, repeated remove() calls without an intervening traversal are invalid.
These rules preserve a clear relationship between the cursor and the last element returned.
11. Iterator vs ListIterator
| Capability | Iterator<E> | ListIterator<E> |
|---|---|---|
| Works with general collections | Yes | No; it is for lists |
| Forward traversal | Yes | Yes |
| Backward traversal | No | Yes |
| Reports positional indexes | No | Yes |
| Removes last-returned element | Yes, if supported | Yes, if supported |
| Replaces last-returned element | No | Yes, if supported |
| Inserts at cursor position | No | Yes, if supported |
| Starts at an arbitrary index | No standard index-based constructor | Yes, through listIterator(index) |
Use Iterator for forward traversal through a general collection and for supported removal of the current item. Use ListIterator when you need backward traversal, index information, replacement, or insertion during a list walk.
A Set such as HashSet, TreeSet, or LinkedHashSet has no stable positional index in the List sense, so it does not provide ListIterator. The same applies to map views such as keySet(). If positional traversal is required, copy the elements into a list first.
12. Performance Considerations for ListIterator
The cost of starting a list iterator at a particular index depends on the list implementation:
- For an
ArrayList,listIterator(index)is typically O(1) because the implementation can position the cursor using an index. - For a
LinkedList,listIterator(index)is O(n) in the worst case because it must walk through nodes to reach the requested position, although it can start from the closer end.
Repeatedly creating iterators at arbitrary indexes can therefore be much more expensive for a linked list than for an array list. The underlying data structure matters even when both classes expose the same ListIterator interface.
13. Making a Custom Class Iterable
A custom class can support the enhanced for loop by implementing Iterable<T> and providing an iterator() method. This lets the class expose a traversal interface without exposing its internal storage directly.
For example, an OrderHistory class can keep a private list of orders and delegate iterator creation to that list. Callers can then write an enhanced for loop over the OrderHistory object, while the class remains responsible for its storage details.
This separation provides useful flexibility. The internal implementation could change from an ArrayList to a LinkedList, or to another source that supplies a cursor, while client code using the enhanced for loop remains largely unchanged.
13.1 Delegating to an existing collection
When the required traversal order matches the internal collection, delegation is usually the simplest implementation. The class returns the internal collection's iterator rather than reimplementing cursor logic.
This reduces custom code and delegates normal traversal behavior to a well-tested collection implementation. It also means the iterator's behavior reflects the underlying collection's rules, including whether removal is supported.
14. Writing a Custom Iterator
Sometimes delegation is insufficient. An OrderHistory object might need to present orders in reverse chronological order without reversing or modifying the underlying list. In that case, the class can return a custom iterator that starts at the last index and decrements its cursor after each returned order.
The core design is:
- Initialize the cursor to the final valid index.
hasNext()returnstruewhile the cursor is a valid index.next()checks availability, returns the element at the cursor, and moves the cursor toward the beginning.- If
next()is called after the iterator runs out of elements, throwNoSuchElementException.
The contract matters even for custom iterators. Callers expect next() to throw NoSuchElementException when no element remains. The default Iterator.remove() implementation throws UnsupportedOperationException, which is appropriate when deletion is not supported by the custom traversal. Override remove() only when removal can be defined safely and consistently for the data structure.
15. Common Mistakes and Best Practices
15.1 Removing directly from the collection during iteration
Avoid structurally modifying a standard collection through a separate collection method while an iterator is traversing it. Use the iterator's remove() method or removeIf() for supported cases.
15.2 Calling next() without checking availability
A call to next() after the end throws NoSuchElementException. Use the conventional while (iterator.hasNext()) pattern when manually controlling traversal.
15.3 Calling remove() before next()
The iterator has no last-returned element to remove until a successful traversal call has occurred. Calling remove() too early—or twice for the same returned element—causes IllegalStateException for standard iterators that follow this contract.
15.4 Assuming fail-fast means thread-safe
ConcurrentModificationException is a best-effort detection mechanism. It does not synchronize access, guarantee detection of every invalid change, or make concurrent modification safe.
15.5 Using ListIterator for a Set or Map view
ListIterator is specific to lists because its extra operations depend on a positional cursor. Use an ordinary Iterator for sets and map views, or copy elements into a list when index-based operations are genuinely required.
15.6 Overusing custom iterator implementations
If the underlying collection already provides the desired order, delegate to its iterator. Implement a custom cursor only when a different traversal order or specialized behavior is needed.
15.7 Ignoring implementation-dependent performance
The same interface can hide different costs. Removal from an ArrayList may shift many elements, while a LinkedList iterator can unlink a reached node efficiently. Starting at an arbitrary index is also much cheaper for an ArrayList than for a LinkedList.
16. Quick Revision Summary
Iterable<T>supplies an iterator throughiterator().Iterator<T>traverses forward usinghasNext()andnext().Iterator.remove()removes the most recently returned element when supported.- Calling
next()beyond the end throwsNoSuchElementException. - Invalid
remove()sequencing can causeIllegalStateException. - Structural changes outside an iterator can trigger
ConcurrentModificationExceptionin fail-fast collections. - Fail-fast behavior is best-effort detection, not a thread-safety guarantee.
Collection.removeIf()is convenient for deleting all elements matching a predicate.ListIterator<E>adds backward movement, index reporting,set(), andadd()for lists.nextIndex()andpreviousIndex()describe the next traversal positions.- A custom class can support enhanced
forby implementingIterable<T>. - A custom iterator should follow the iterator contract, including throwing
NoSuchElementExceptionwhen traversal is exhausted.
17. Conclusion
Iterator and ListIterator provide structured ways to traverse Java collections. The basic iterator is suitable for forward traversal across general collections and supports safe removal through its own protocol. ListIterator adds the richer capabilities needed to walk lists in both directions, inspect cursor positions, replace elements, and insert elements during traversal.
Understanding the cursor model makes the method rules easier to remember. It also helps prevent common errors involving invalid removal, exhausted iterators, and external structural modification. For simple predicate-based cleanup, removeIf() is concise; for custom traversal order, a class can implement Iterable and provide its own iterator. Selecting the right mechanism improves clarity, correctness, and performance in collection-processing code.