Java ListIterator add set remove: Modifying Lists
java listiterator add set remove: Learn how to use ListIterator's add, set, and remove methods to modify Java lists safely during iteration, including constraints and...
java listiterator add set remove requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you need to modify a Java list while iterating over it, the ListIterator interface provides add, set, and remove methods that avoid the ConcurrentModificationException you get with a plain Iterator. This article explains how these methods behave, the constraints around their use, and practical examples.
Why ListIterator Instead of Iterator?
The standard Iterator interface only supports remove() during iteration. If you try to call add or set on the underlying list while iterating with an Iterator, you risk throwing a ConcurrentModificationException because the list's structural modification count changes. ListIterator extends Iterator and adds add(E e) and set(E e) methods that are designed to work safely with the iterator's internal cursor. It also supports bidirectional traversal with previous() and next(), making it the right choice when you need to insert or replace elements at the current position.
Here is a quick comparison of the two interfaces:
| Feature | Iterator | ListIterator |
|---|---|---|
| Traversal direction | Forward only | Bidirectional |
remove() | Yes | Yes |
add() | No | Yes |
set() | No | Yes |
| Index access | No | Yes (nextIndex(), previousIndex()) |
How the add Method Works
The add(E e) method inserts the specified element into the list immediately before the element that would be returned by next() and after the element that would be returned by previous(). The cursor position is advanced by one, so a subsequent call to previous() returns the newly added element. This behavior is useful when you need to insert elements at a specific position while iterating.
List<String> list = new ArrayList<>(List.of("a", "c")); ListIterator<String> it = list.listIterator(); it.next(); // returns "a" it.add("b"); // inserts "b" after "a" System.out.println(list); // [a, b, c]
In this example, it.next() moves the cursor to after "a". The add call inserts "b" at that position. The cursor now points between "b" and "c". Calling it.previous() would return "b".
Using set to Replace the Last Returned Element
The set(E e) method replaces the last element returned by next() or previous(). It can only be called after a call to next() or previous() and before any call to remove() or add(). If you call set without a prior next() or previous(), it throws IllegalStateException. This method does not change the cursor position.
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3)); ListIterator<Integer> it = numbers.listIterator(); it.next(); // returns 1 it.set(10); // replaces 1 with 10 System.out.println(numbers); // [10, 2, 3]
The set method is particularly useful when you need to update elements in place without creating a new list. It works with both next() and previous(), so you can modify elements during reverse iteration as well.
The remove Method and Its Constraints
The remove() method removes the last element returned by next() or previous(). Like set, it must be called after a next() or previous() call, and it can be called only once per such call. After calling remove(), you must call next() or previous() again before calling remove() or set(). Attempting to call remove() twice in a row without an intervening traversal throws IllegalStateException.
List<String> list = new ArrayList<>(List.of("x", "y", "z")); ListIterator<String> it = list.listIterator(); it.next(); // returns "x" it.remove(); // removes "x" System.out.println(list); // [y, z]
Note that remove() does not affect the cursor position in the same way as add. After removing the last returned element, the cursor remains between the previous and next elements. If you call previous() after a remove(), it returns the element before the removed one, which can lead to unexpected behavior if you are not careful.
Order of Operations and Common Pitfalls
The three methods have strict ordering rules that are easy to violate. The table below summarizes the valid sequences:
| Operation | Allowed after next()/previous() | Allowed after add() | Allowed after remove() |
|---|---|---|---|
set() | Yes | No | No |
remove() | Yes | No | No |
add() | Yes | Yes | Yes |
A common mistake is calling set() or remove() after add(). The ListIterator contract requires that set and remove operate on the element most recently returned by next() or previous(). Since add() does not return an element, it invalidates any previous next() or previous() result. Similarly, calling remove() twice without an intervening traversal is illegal.
Another pitfall is using ListIterator on a fixed-size list such as Arrays.asList. The add and remove methods throw UnsupportedOperationException because these lists do not support structural modification. set works fine on such lists, but you must be aware of the list's implementation before using add or remove.
Practical Example: Filtering and Replacing in One Pass
A common use case is to iterate over a list and both remove certain elements and replace others. With ListIterator, you can do this in a single pass without creating a separate collection.
List<String> words = new ArrayList<>(List.of("apple", "banana", "cherry", "date")); ListIterator<String> it = words.listIterator(); while (it.hasNext()) { String word = it.next(); if (word.startsWith("a")) { it.remove(); } else if (word.length() > 5) { it.set(word.toUpperCase()); } } System.out.println(words); // [BANANA, CHERRY]
In this example, "apple" is removed, and "banana" and "cherry" are converted to uppercase. The ListIterator handles the cursor correctly across both operations, so you don't need to manage index offsets manually.
Performance and Thread-Safety Considerations
ListIterator is not thread-safe. If multiple threads modify the same list concurrently, you must synchronize externally or use a concurrent collection. The iterator's internal cursor and the list's modCount are not protected, so concurrent modification can still throw ConcurrentModificationException even when using ListIterator. For single-threaded code, the methods are efficient: add, set, and remove on an ArrayList have O(n) time complexity because shifting elements may be required, while LinkedList offers O(1) for add and remove at the current position. When choosing a list implementation, consider the frequency of structural changes. If you need to insert or remove many elements at arbitrary positions, LinkedList may be more appropriate than ArrayList, despite its higher per-element memory overhead.
Another operational concern is that ListIterator is a view of the list at the time of creation. If you modify the list outside the iterator (e.g., by calling list.add() directly), the iterator becomes invalid and will throw ConcurrentModificationException on the next traversal. Always use the iterator's own methods to modify the list while iterating.
For large lists, using ListIterator with previous() and set() can be more efficient than repeatedly getting elements by index, because it avoids re-traversing the list from the beginning. However, the difference is usually negligible unless the list is very large or the operation is in a hot loop. In such cases, measuring the actual performance with your data is the only reliable way to decide between iterator-based and index-based modification.