Iterator and ListIterator

Every for-each loop over a collection quietly creates an Iterator: a small object with a cursor that knows how to step through that collection, whether it's an array inside, a chain of nodes or a tree. Using one directly lets you do what a for-each can't: remove elements safely while looping, and, with a ListIterator, walk backwards, replace and insert at the cursor. You'll also see why changing a list behind an iterator's back throws ConcurrentModificationException.

Module 11 · Collections⏱ 20 minIntermediate8 practice problems
1 The story

The ticket checker's finger

On the Panvel local, the ticket checker walks the compartment keeping one finger on the passenger he's checking. When a ticketless passenger is taken off at Kharghar, he removes them and his finger stays in the right place. Kabir, coding the same thing, removed passengers inside a for-each loop. His program crashed.

Riya no ticket Om ▲ cursor it.remove();✓ safe: cursor adjusts list.remove(p);✗ inside for-each for (String p : list) if (noTicket(p)) list.remove(p); ConcurrentModificationException the hidden iterator saw the list change myphatshala.in
Riya

Sir, Kabir's loop is just a for-each. Where's the iterator?

Arjun Sir

Hidden. javac turns every for-each over a collection into Iterator it = list.iterator(); while (it.hasNext()) { p = it.next(); … }. When Kabir calls list.remove, the list changes without telling that iterator.

Riya

And the iterator notices?

Arjun Sir

Every list keeps a change counter. The iterator remembers the value it started with and checks it on every next(). If someone else changed the list, it stops immediately rather than return wrong data. Remove through the iterator itself, like the checker's finger, and everything stays consistent.

2 Definition

What are Iterator and ListIterator?

Formal definition (write this in exams)

An Iterator<E> (java.util) is an object that traverses the elements of a collection one at a time, obtained from collection.iterator(). It defines hasNext(), next() and the optional remove(), which removes the last element returned and is the only safe way to modify a collection during iteration. The enhanced for loop uses an iterator for any Iterable. Iterators of most java.util collections are fail-fast: if the collection is structurally modified other than through the iterator, the next operation throws ConcurrentModificationException. A ListIterator<E>, obtained from list.listIterator(), extends Iterator for lists with bidirectional movement (hasPrevious, previous), index queries (nextIndex, previousIndex) and modification at the cursor (set, add).

In simple words

An iterator is a finger moving through a collection. A ListIterator is a finger that can also go backwards and edit the list where it points.

Words you'll meet in this lesson

CursorThe iterator's position, between elements. next() jumps over one element and returns it.
Structural changeAdding or removing elements (not just set).
modCountA counter in the collection, increased on every structural change.
Fail-fastThrow ConcurrentModificationException as soon as an unexpected change is seen.
Fail-safeIterate over a snapshot or tolerate changes (concurrent collections).
IterableAny type with an iterator() method; usable in for-each.

The methods

MethodIteratorListIteratorWhat it does
hasNext() / next()✓✓Is there more? Return the next element.
remove()✓✓Remove the element last returned by next/previous.
hasPrevious() / previous()✓Walk backwards.
nextIndex() / previousIndex()✓Index the cursor is next to.
set(e)✓Replace the element last returned.
add(e)✓Insert at the cursor.

Exam tip: remove() may be called once per next(); calling it twice, or before any next(), throws IllegalStateException. next() past the end throws NoSuchElementException. Modifying the collection directly during iteration throws ConcurrentModificationException (fail-fast).

3 Types and ways to use it

Four iterator patterns

Walk

while (it.hasNext()) it.next(): what for-each does for you.

read

Remove safely

it.remove() during the loop. Or removeIf.

filter in place

Walk back, edit

ListIterator: previous, set, add.

lists only

Your own Iterable

Implement iterator(); your class works in for-each.

custom

What for-each really does

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> coach = new ArrayList<>(List.of("Riya", "Kabir", "Om"));

        for (String p : coach) System.out.print(p + ";");      // you write this…
        System.out.println();

        Iterator<String> it = coach.iterator();                // …javac writes this
        while (it.hasNext()) {
            String p = it.next();
            System.out.print(p + ";");
        }
        System.out.println();
    }
}
Riya;Kabir;Om; Riya;Kabir;Om;

Removing while iterating, the right way

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Integer> marks = new ArrayList<>(List.of(78, 32, 91, 28, 64));
        Iterator<Integer> it = marks.iterator();
        while (it.hasNext()) {
            if (it.next() < 35) {
                it.remove();                       // removes the element just returned
            }
        }
        System.out.println(marks);

        List<Integer> copy = new ArrayList<>(List.of(78, 32, 91, 28, 64));
        copy.removeIf(m -> m < 35);               // the same in one line (Lesson 14.1)
        System.out.println(copy);
    }
}
[78, 91, 64] [78, 91, 64]

ListIterator: backwards, set and add

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> route = new ArrayList<>(List.of("Panvel", "Kharghar", "Vashi"));
        ListIterator<String> li = route.listIterator();

        while (li.hasNext()) {
            int i = li.nextIndex();
            String stop = li.next();
            if (stop.equals("Kharghar")) {
                li.set("Kharghar (Fest)");          // replace the element just returned
                li.add("Belapur");                  // insert right after it, at the cursor
            }
            System.out.println(i + ": " + stop);
        }
        System.out.print("Back: ");
        while (li.hasPrevious()) System.out.print(li.previous() + "|");
        System.out.println();
    }
}
0: Panvel 1: Kharghar 3: Vashi Back: Vashi|Belapur|Kharghar (Fest)|Panvel|

Notice two things: “Belapur” wasn't printed in the forward loop, because add inserts before the cursor and the next next() continues with Vashi; and Vashi's index is now 3, because Belapur took index 2.

Making your own class iterable

import java.util.Iterator;

class Countdown implements Iterable<Integer> {
    private final int from;
    Countdown(int from) { this.from = from; }

    public Iterator<Integer> iterator() {
        return new Iterator<>() {                // anonymous class (Lesson 10.5)
            int current = from;
            public boolean hasNext() { return current > 0; }
            public Integer next() { return current--; }
        };
    }
}

public class Main {
    public static void main(String[] args) {
        for (int n : new Countdown(3)) System.out.print(n + " ");
        System.out.println("Launch!");
    }
}
3 2 1 Launch!

Don't change a collection behind its iterator. list.add or list.remove inside a for-each over the same list throws ConcurrentModificationException (except by luck when removing the second-to-last element, as Lesson 11.3 showed). Use it.remove(), ListIterator.add, removeIf, or collect changes and apply them after the loop.

Rule of thumb: for-each for reading; an explicit Iterator when you need to remove; ListIterator when you need to replace, insert or go backwards in a list. For collections shared between threads, use concurrent collections whose iterators don't throw (Module 12).

4 Step by step

How fail-fast checking works

  1. The list counts its changes

    ArrayList inherits a field modCount from AbstractList. Every add/remove/clear increments it; set doesn't (no structural change).
  2. The iterator takes a note

    int cursor;                       // index of the next element
    int lastRet = -1;                 // index of the last element returned
    int expectedModCount = modCount;  // the count when iteration started
    list.iterator() creates an ArrayList.Itr, an inner class (Lesson 10.5) that reads the list's private array through its outer link.
  3. Every next() checks

    next() first calls checkForComodification(): if modCount != expectedModCount, throw ConcurrentModificationException. Otherwise return elementData[cursor], set lastRet = cursor and advance.
  4. it.remove() keeps them in sync

    It removes index lastRet from the list (which bumps modCount), moves cursor back to lastRet, sets lastRet = -1 (so a second remove fails) and updates expectedModCount. No exception.
  5. Best effort, not a guarantee

    Fail-fast checks catch most bugs, but the Java docs warn they're not guaranteed, especially across threads. Treat the exception as a bug detector, never as program logic.
5 See it happen

The cursor and the change counter: both views

Left: a loop that removes failing marks. Right: the iterator's cursor, lastRet and expectedModCount next to the list's array and modCount.

How an iterator stays safe 6 steps
What you see it = marks.iterator(); it.next(); // 78 it.next(); // 32 it.remove(); it.next(); // 91 marks.remove(0); it.next(); marks [78, 32, 91, 28] [78, 32, 91, 28] → 78 [78, 32, 91, 28] → 32 [78, 91, 28] [78, 91, 28] → 91 ConcurrentModificationException Get an iterator. First element. Second element. Remove 32 through the iterator. Carry on: 91. Change the list directly… myphatshala.in
Behind the screen 78 32 91 28 78 91 28 91 28 0 1 2 3 4 ▲ cursor ▲ cursor ▲ cursor ▲ cursor ▲ cursor ▲ cursor list elementData modCount 0 0 0 1 1 2 Itr object cursor = 0 lastRet = -1 expectedModCount = 0 cursor = 1 lastRet = 0 expectedModCount = 0 cursor = 2 lastRet = 1 expectedModCount = 0 cursor = 1 lastRet = -1 expectedModCount = 1 cursor = 2 lastRet = 1 expectedModCount = 1 cursor = 2 lastRet = 1 expectedModCount = 1 2 ≠ 1 → throw Cursor before index 0; counts match. Return [0], cursor → 1. Return [1], cursor → 2. Remove [lastRet]; cursor back to 1; counts re-synced. Return [1] = 91. Direct change: counts disagree. myphatshala.in
6 Real-world use

Iterators in real systems

Iterators matter whenever code walks large or changing data, and when you build your own data types.

Watch it with an example: clearing expired OTPs

A login service keeps one-time passwords in a map with their expiry. A cleanup job walks the entries with an iterator and removes expired ones in place.

Removing expired entries with an iterator 5 steps
Animated example riya → OTP (valid) kabir → OTP (expired) kabir: removed om → OTP (valid) zoya → OTP (expired) zoya: removed ◀ it ◀ it ◀ it ◀ it ◀ it it.next() valid: keep expired! it.remove() safe, in place Riya's OTP is still valid. Kabir's has expired. Remove it through the iterator. Om's is valid. Zoya's expired: removed. myphatshala.in
expired sessionsit.remove()

1. Cleaning up in place

Background jobs remove expired sessions, cancelled orders or stale cache entries while scanning, using iterator.remove() or removeIf on the collection or a map's entrySet().

scan entries→it.remove()→no CME, no copy
rs.next()reader.readLine()

2. Streaming large data

Database result sets, file readers and paged API clients follow the same “has next / get next” idea, so millions of rows can be processed without loading them all.

while (rs.next())→one row at a time→constant memory
implements Iterable

3. Custom collections

A Playlist, Route or Portfolio class that implements Iterable works in for-each and hides how it stores its items.

for (Song s : playlist)→playlist.iterator()→internals hidden
CopyOnWriteArrayListsnapshot iterator

4. Listener lists

Event systems keep listeners in a CopyOnWriteArrayList: iterating (firing events) is frequent, changes are rare, and its iterator walks a snapshot, so a listener can unregister itself mid-event without an exception.

fire event→snapshot iteration→safe removal

Examples are simplified. JDBC's ResultSet isn't a java.util.Iterator, but follows the same pattern.

7 Run the code

Checking tickets on a train

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> coach = new ArrayList<>(List.of("Riya:T", "Kabir:-", "Om:T", "Zoya:-", "Meera:T"));
        List<String> fined = new ArrayList<>();

        Iterator<String> it = coach.iterator();
        while (it.hasNext()) {
            String[] p = it.next().split(":");
            if (p[1].equals("-")) {               // no ticket
                fined.add(p[0]);
                it.remove();                      // safe removal at the cursor
            }
        }

        ListIterator<String> li = coach.listIterator();
        while (li.hasNext()) {
            String name = li.next().split(":")[0];
            li.set(name + " ✓");                  // mark as checked
        }

        System.out.println("Fined: " + fined);
        System.out.println("Checked: " + coach);
    }
}
Fined: [Kabir, Zoya] Checked: [Riya ✓, Om ✓, Meera ✓]
  • coach.iterator()A fresh iterator with its cursor before the first passenger.
  • it.remove()Removes the passenger just returned by next(); the iterator adjusts.
  • listIterator() + setReplaces each element in place as we pass it.
  • Adding to finedFine: it's a different list from the one being iterated.
8 Line by line

The iterator object on the heap

This trace is recorded from the real JVM. The iterator is an object, ArrayList.Itr, with a cursor (index of the next element) and lastRet (index last returned). Watch it.remove() move the cursor back so nothing is skipped.

Worked examples: real problems, solved and traced in memory

Three iterator jobs from real apps: safe removal, editing at the cursor and a custom iterable.

Example 1Login · map cleanup

Remove expired OTPs from a map

Problem statement

OTPs are stored as user → expiry minute. At minute 10, remove every entry whose expiry is before 10, while iterating.

Input
{riya=12, kabir=8, om=15, zoya=9}, now = 10
Expected output
{riya=12, om=15}

Approach

  1. Iterate over entrySet().iterator().
  2. If an entry's value is less than now, call it.remove().
  3. Print what remains.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 2Exams · ListIterator

Apply grace marks in place

Problem statement

Students who scored 33 or 34 get grace marks up to 35. Update the list in place with a ListIterator's set, and count how many were changed.

Input
[33, 72, 34, 20]
Expected output
[35, 72, 35, 20], grace given to 2

Approach

  1. Walk with listIterator().
  2. If a mark is 33 or 34, set(35) replaces it where the cursor just passed.
  3. Count the changes.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 3Fitness · custom Iterable

Iterate over a workout plan's days

Problem statement

A Plan stores exercises per day. Make it Iterable<String> so a for-each prints each day's line.

Input
Plan with "Push", "Pull", "Legs"
Expected output
Day 1: Push
Day 2: Pull
Day 3: Legs

Approach

  1. Plan keeps a private String array.
  2. iterator() returns an anonymous Iterator with an index.
  3. main uses for-each; Plan's storage stays hidden.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

9 Quiz and revision

Check yourself

Quick revision

Lesson 11.7 in one screen

  • Iterator: hasNext(), next(), remove(). for-each = hidden iterator.
  • Remove while looping: it.remove() (once per next) or removeIf. Never list.remove inside for-each.
  • Fail-fast: modCount vs expectedModCount → ConcurrentModificationException. Best effort only.
  • Errors: second remove → IllegalStateException; next past end → NoSuchElementException.
  • ListIterator: hasPrevious/previous, nextIndex/previousIndex, set, add (at the cursor).
  • Implement Iterable to use your class in for-each. Concurrent collections have fail-safe iterators.

Interview and viva questions

What is ConcurrentModificationException and how do you avoid it?

It's thrown by fail-fast iterators when a collection is structurally modified during iteration by anything other than the iterator itself. Avoid it by removing through Iterator.remove(), using removeIf, collecting changes and applying them after the loop, iterating over a copy, or using a concurrent collection. It can happen in a single thread; it's not only a threading issue.

Fail-fast vs fail-safe iterators?

Fail-fast iterators (ArrayList, HashMap…) throw on concurrent modification. Fail-safe (more precisely weakly consistent or snapshot) iterators, as in CopyOnWriteArrayList and ConcurrentHashMap, don't throw: they iterate over a snapshot or tolerate concurrent changes, possibly not reflecting the latest updates.

Iterator vs ListIterator?

Iterator works on any Collection and moves forward only, with remove. ListIterator works on Lists, moves both ways, knows indexes, and can set and add at the cursor.

Iterator vs Enumeration?

Enumeration (hasMoreElements, nextElement) is the legacy interface of Vector and Hashtable. Iterator replaced it with shorter names and a remove method.

How do you remove entries from a HashMap while iterating?

Iterate over map.entrySet().iterator() and call it.remove(), or use map.entrySet().removeIf(e -> …) / map.values().removeIf(…). Calling map.remove(key) inside a for-each over the map throws ConcurrentModificationException.

Practice problems

Iterate, remove and edit safely. The last three are business tasks.

P1Easy

Walk with an explicit iterator

Print the elements of ["UPI", "Card", "Cash"] using hasNext/next.

Input
["UPI", "Card", "Cash"]
Expected output
UPI
Card
Cash
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Iterator<String> it = List.of("UPI", "Card", "Cash").iterator();
        while (it.hasNext()) System.out.println(it.next());
    }
}
UPI Card Cash

How it works: exactly what a for-each compiles to.

P2Easy

Remove empty strings

Remove all empty strings from ["a", "", "b", ""] using an iterator.

Input
["a", "", "b", ""]
Expected output
[a, b]
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> l = new ArrayList<>(List.of("a", "", "b", ""));
        Iterator<String> it = l.iterator();
        while (it.hasNext()) if (it.next().isEmpty()) it.remove();
        System.out.println(l);
    }
}
[a, b]

How it works: it.remove() keeps the iterator consistent, so adjacent empty strings aren't skipped.

P3Easy

Print a list backwards

Print [1, 2, 3, 4] in reverse using a ListIterator started at the end.

Input
[1, 2, 3, 4]
Expected output
4 3 2 1 
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Integer> l = List.of(1, 2, 3, 4);
        ListIterator<Integer> li = l.listIterator(l.size());
        while (li.hasPrevious()) System.out.print(li.previous() + " ");
        System.out.println();
    }
}
4 3 2 1

How it works: listIterator(size) starts the cursor after the last element; previous() walks back.

P4Medium

Double every even number in place

In [1, 2, 3, 4] replace each even number with double its value; remove odd numbers.

Input
[1, 2, 3, 4]
Expected output
[4, 8]
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Integer> l = new ArrayList<>(List.of(1, 2, 3, 4));
        ListIterator<Integer> li = l.listIterator();
        while (li.hasNext()) {
            int v = li.next();
            if (v % 2 == 0) li.set(v * 2);
            else li.remove();
        }
        System.out.println(l);
    }
}
[4, 8]

How it works: set and remove both act on the element last returned by next().

P5Medium

Interleave a separator

Insert "|" between every pair of items in [A, B, C] using ListIterator.add.

Input
[A, B, C]
Expected output
[A, |, B, |, C]
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> l = new ArrayList<>(List.of("A", "B", "C"));
        ListIterator<String> li = l.listIterator();
        while (li.hasNext()) {
            li.next();
            if (li.hasNext()) li.add("|");
        }
        System.out.println(l);
    }
}
[A, |, B, |, C]

How it works: add inserts at the cursor, before the next element, and the loop continues with that next element, so separators aren't revisited.

P6Business problem

Drop out-of-stock products

Products as "name:stock": [Pen:4, Ink:0, Book:2, Clip:0]. Remove those with 0 stock and print the rest.

Input
4 products
Expected output
[Pen:4, Book:2]
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> products = new ArrayList<>(List.of("Pen:4", "Ink:0", "Book:2", "Clip:0"));
        Iterator<String> it = products.iterator();
        while (it.hasNext()) {
            String p = it.next();
            if (p.endsWith(":0")) it.remove();
        }
        System.out.println(products);
    }
}
[Pen:4, Book:2]

How it works: safe in-place filtering in one pass.

P7Business problem

Mask phone numbers in a list

Replace each phone number in a contact list with a masked form (last 4 digits visible) using set.

Input
["9876543210", "9123456789"]
Expected output
[XXXXXX3210, XXXXXX6789]
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> phones = new ArrayList<>(List.of("9876543210", "9123456789"));
        ListIterator<String> li = phones.listIterator();
        while (li.hasNext()) {
            String p = li.next();
            li.set("XXXXXX" + p.substring(6));
        }
        System.out.println(phones);
    }
}
[XXXXXX3210, XXXXXX6789]

How it works: set replaces in place without changing the list's size, so it's not a structural change.

P8Business problem

Pages of results as an Iterable

Write Pages that iterates over a list in pages of size 2, so a for-each gets one page (a sub-list) at a time.

Input
[O1..O5], page size 2
Expected output
[O1, O2]
[O3, O4]
[O5]
Show solution and explanation
import java.util.*;

class Pages implements Iterable<List<String>> {
    private final List<String> all;
    private final int size;

    Pages(List<String> all, int size) { this.all = all; this.size = size; }

    public Iterator<List<String>> iterator() {
        return new Iterator<>() {
            int from = 0;
            public boolean hasNext() { return from < all.size(); }
            public List<String> next() {
                List<String> page = all.subList(from, Math.min(from + size, all.size()));
                from += size;
                return page;
            }
        };
    }
}

public class Main {
    public static void main(String[] args) {
        for (List<String> page : new Pages(List.of("O1", "O2", "O3", "O4", "O5"), 2)) {
            System.out.println(page);
        }
    }
}
[O1, O2] [O3, O4] [O5]

How it works: callers just loop over pages; the paging logic lives inside the iterator.

Real-world scenario questions (asked in company interviews)

Iterator questions are usually ConcurrentModificationException stories. Explain the mechanism, then the fix.

Backend · bugFresher

CME in a single-threaded job

A colleague says “ConcurrentModificationException means two threads are fighting”. But the failing job runs on one thread: it loops over orders and calls orders.remove(o) for cancelled ones. Explain.

The interviewer is checking: what “concurrent” means here.

Show a strong answer
  • “Concurrent” means “during iteration”, not “multi-threaded”. The for-each's hidden iterator saw modCount change because the list was modified directly.
  • Fix: orders.removeIf(Order::isCancelled), or an explicit iterator with it.remove().
  • Add a test with two adjacent cancelled orders to make sure none is skipped.

Avoid: switching to CopyOnWriteArrayList just to silence it in single-threaded code; it copies the whole array on every write.

Caching · mapsJunior

Evicting expired cache entries

A cache is a HashMap<String, Entry>. A cleanup task does for (String k : cache.keySet()) if (expired(k)) cache.remove(k); and crashes. How do you write it correctly?

The interviewer is checking: removing from maps during iteration.

Show a strong answer
  • cache.entrySet().removeIf(e -> e.getValue().isExpired());, or an explicit Iterator<Map.Entry<…>> with it.remove().
  • If other threads read/write the cache, make it a ConcurrentHashMap, whose iterators don't throw and whose removeIf is safe.
  • Consider a cache library with built-in expiry.

Avoid: catching the exception and restarting the loop. It's O(n²) and hides the bug.

Library · API designJunior

Exposing a class's items without exposing the list

A Playlist class stores songs in a private ArrayList. Callers want to loop over songs with for-each, but must not add or remove songs directly. How?

The interviewer is checking: Iterable and encapsulation.

Show a strong answer
  • Make Playlist implements Iterable<Song> and return Collections.unmodifiableList(songs).iterator(), whose remove() throws UnsupportedOperationException.
  • Offer explicit methods (addSong, removeSong) that apply business rules.
  • The internal list can later change type without affecting callers.

Avoid: a getter returning the internal ArrayList. Any caller could clear() it.

Events · concurrencyMid

A listener that unsubscribes itself

An event bus loops over its listeners and calls each one. One listener unsubscribes itself when called, and the bus crashes with ConcurrentModificationException. What are your options?

The interviewer is checking: snapshot iteration and its trade-offs.

Show a strong answer
  • Store listeners in a CopyOnWriteArrayList: iteration walks a snapshot, so unsubscribing during an event is safe. Ideal when events are frequent and subscriptions rare.
  • Alternatively iterate over a copy (new ArrayList<>(listeners)) per event, or queue unsubscriptions and apply them after the loop.
  • Note the semantics: the removed listener may still receive the current event.

Avoid: synchronising the whole loop if listeners can call back into the bus; it risks deadlocks.