Iterators in Java — practice with safe deletes, a linked list of your own and a two-way playlist (and the second for-each that printed nothing)

My first Iterable class worked on the first try. I dropped it into a for-each, the whole list came out and I felt like a genius for almost a minute. Then I wanted to compare every element with all the others and nested two for-each loops over the same collection. The outer one went around once… and the inner one printed nothing. No error, no exception: silence. It took me a whole afternoon to realise that I had stored the iterator in an attribute of the class, so both loops shared the same cursor, and the inner one always left it at the end.

That’s the beauty of this topic: an iterator is a cursor, and what happens with it is very easy to understand… once you see it move. That’s why today we practise with three programs where you’ll watch the cursor, and break it, yourself.

If you’ve already read the iterators theory, this article is for applying it: deleting from a list without the exception being thrown, building a collection of your own that works in a for-each, and walking a playlist in both directions. They’re medium-difficulty programs. Each one comes with commented code, the output and the doubts that usually come up.

Before you start

It helps to be clear about:

  • That the for-each is an Iterator in disguise: it asks the Iterable for an iterator and repeats hasNext() and next().
  • That to delete while you traverse you have to use it.remove() (once per next()), not the list’s own remove.
  • That an Iterable returns a new iterator on every call to iterator(), and that each iterator keeps its own cursor.
  • That ListIterator lets you go backwards and modify the list with set() and add().

If any of these sounds shaky, review the theory before continuing.

The three programs are independent. Create a folder for each one and compile them separately. Programs 1 and 3 share the same Song class; program 2 doesn’t need it.

Program 1: deleting with Iterator without an exception

A playlist with five songs, three of which are too short (under two and a half minutes). We want to remove them in three different ways and see which one behaves.

The idea

We’ll do the same thing three times: delete the songs shorter than 150 seconds. First with a for-each and the list’s remove (to see the exception and, above all, how the list ends up afterwards). Then with Iterator.remove(), the safe way. And finally with the removeIf shortcut.

In the first part we catch the ConcurrentModificationException with a try/catch so we can carry on. You wouldn’t do that in real code: it’s only to show you the state the list is left in.

Before running it, think about how many songs the first version will delete. All three? One? We’ll check afterwards.

The code

Song.java

public record Song(String title, int seconds) {
    @Override
    public String toString() {
        return title + " (" + seconds / 60 + ":" + String.format("%02d", seconds % 60) + ")";
    }
}

DeleteSongs.java

import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;

public class DeleteSongs {
    static List<Song> load() {
        return new ArrayList<>(List.of(
                new Song("Dawn", 215), new Song("Intro", 45),
                new Song("Highway", 180), new Song("Lighthouse", 135),
                new Song("Silence", 20)));
    }

    public static void main(String[] args) {
        // 1. for-each + remove(): danger
        List<Song> a = load();
        try {
            for (Song c : a) {
                if (c.seconds() < 150) {
                    a.remove(c);
                }
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("1. for-each: " + e.getClass().getSimpleName());
        }
        System.out.println("   Left half-done: " + a);

        // 2. Iterator.remove(): the safe way
        List<Song> b = load();
        Iterator<Song> it = b.iterator();
        int deleted = 0;
        while (it.hasNext()) {
            if (it.next().seconds() < 150) {
                it.remove();
                deleted++;
            }
        }
        System.out.println("2. Iterator: deleted " + deleted + " -> " + b);

        // 3. The shortcut: removeIf
        List<Song> c = load();
        c.removeIf(x -> x.seconds() < 150);
        System.out.println("3. removeIf: " + c);
    }
}

Output

1. for-each: ConcurrentModificationException
   Left half-done: [Dawn (3:35), Highway (3:00), Lighthouse (2:15), Silence (0:20)]
2. Iterator: deleted 3 -> [Dawn (3:35), Highway (3:00)]
3. removeIf: [Dawn (3:35), Highway (3:00)]

Check it by hand:

  • **for-each + remove():** the traversal reaches Intro (0:45), deletes it with a.remove(c) and, on the next next(), the exception is thrown. The list is left half-done: Intro is gone, but Lighthouse (2:15) and Silence (0:20) are still there, when they should have been deleted too.
  • **Iterator.remove():** deletes all three (Intro, Lighthouse and Silence) and leaves only Dawn and Highway. deleted counts 3.
  • **removeIf:** same result, in one line.

x -> x.seconds() < 150 is a lambda: a small function that removeIf applies to each song to decide whether to delete it. You don’t need to master it now; think of it as “the condition, written short”.

Questions you might ask

**Why isn’t the exception thrown right after the delete, but on the next next()?** Because the one that checks the modification counter is the iterator, and it only does so when you ask it for the next element. The delete itself (a.remove(c)) is done by the list, which knows nothing about iterators.

**Why doesn’t it.remove() cause the exception?** Because the iterator itself does the deleting, and when it does so it updates its own modification count. The list and the iterator are back in agreement.

Why isn’t it enough to catch the exception and carry on? Because the state the list ends up in is unpredictable: here it was left half-done, with two short songs not deleted. The exception isn’t a failure you can ignore, it’s a warning that the traversal can no longer be trusted.

**What if I traverse with an index for loop?** It works, but be careful: when you delete element i, the next one moves into position i and the i++ skips it. It’s fixed by traversing from back to front. With an iterator, you avoid that problem.

Calling it.remove() without having called it.next() first in that iteration, or twice in a row. IllegalStateException is thrown. Each remove() needs its own next().

Try it yourself

  1. Change the condition to delete the songs longer than 200 seconds instead of the short ones, with Iterator.remove().
  2. Modify the program so that, besides deleting them, it prints the title of each deleted song ("Deleted: " + s.title()). Hint: you need to keep the song returned by next() in a variable.

Program 2: my own Iterable collection

A linked list made by hand, without ArrayList: nodes that point to one another. And the important part: it must work in a for-each like any other collection.

The idea

A linked list stores its elements in nodes: each node carries a piece of data and an arrow to the next one. The list only knows the first node (and, for convenience, the last), and to reach any other you have to follow arrows. That’s exactly what an iterator does: its cursor is “the node that’s next to be returned”.

We’ll make the class generic (SinglyLinkedList<T>) so it works with any type, and give it an iterator of its own, as an inner class. Then we’ll test it with a for-each, with two nested for-each loops (each with its own cursor), with an exhausted iterator, and with a method that accepts any Iterable.

Draw three boxes with arrows on paper (FP2 → IC2 → AEDV) and move your finger the way the cursor would. Each time your finger advances, that’s a next().

The code

SinglyLinkedList.java

import java.util.Iterator;
import java.util.NoSuchElementException;

public class SinglyLinkedList<T> implements Iterable<T> {
    private class Node {
        T data;
        Node nextNode;

        Node(T data) {
            this.data = data;
        }
    }

    private Node first;
    private Node last;
    private int count;

    public void add(T data) {
        Node node = new Node(data);
        if (first == null) {
            first = node;
        } else {
            last.nextNode = node;
        }
        last = node;
        count++;
    }

    public int size() {
        return count;
    }

    @Override
    public Iterator<T> iterator() {
        return new LinkedIterator();
    }

    private class LinkedIterator implements Iterator<T> {
        private Node current = first;          // each iterator starts at the first node

        @Override
        public boolean hasNext() {
            return current != null;
        }

        @Override
        public T next() {
            if (current == null) {
                throw new NoSuchElementException();
            }
            T data = current.data;
            current = current.nextNode;
            return data;
        }
    }
}

Linked.java

import java.util.ArrayList;
import java.util.Iterator;
import java.util.NoSuchElementException;

public class Linked {
    static int count(Iterable<?> collection) {
        int n = 0;
        for (Object o : collection) {
            n++;
        }
        return n;
    }

    public static void main(String[] args) {
        SinglyLinkedList<String> subjects = new SinglyLinkedList<>();
        subjects.add("FP2");
        subjects.add("IC2");
        subjects.add("AEDV");

        System.out.println("With for-each:");
        for (String a : subjects) {
            System.out.println("  " + a);
        }

        System.out.println("Two nested for-each loops (each with its own cursor):");
        for (String a : subjects) {
            for (String b : subjects) {
                if (!a.equals(b)) {
                    System.out.println("  " + a + " + " + b);
                }
            }
        }

        Iterator<String> it = subjects.iterator();
        for (int i = 0; i < subjects.size(); i++) {
            it.next();
        }
        try {
            it.next();
        } catch (NoSuchElementException e) {
            System.out.println("Exhausted iterator: " + e.getClass().getSimpleName());
        }

        ArrayList<Integer> numbers = new ArrayList<>();
        numbers.add(7);
        numbers.add(8);
        System.out.println("count(my list) = " + count(subjects));
        System.out.println("count(ArrayList) = " + count(numbers));
    }
}

Output

With for-each:
  FP2
  IC2
  AEDV
Two nested for-each loops (each with its own cursor):
  FP2 + IC2
  FP2 + AEDV
  IC2 + FP2
  IC2 + AEDV
  AEDV + FP2
  AEDV + IC2
Exhausted iterator: NoSuchElementException
count(my list) = 3
count(ArrayList) = 2

Check it by hand:

  • **for-each:** the three subjects, in the order they were added.
  • **Two nested for-each loops:** that’s 6 pairs (3 × 3 minus the 3 where a and b are the same). Each for-each asks for its own iterator, and the two traverse the list at the same time without getting in each other’s way.
  • Exhausted iterator: after three next() calls, the cursor is at null and the fourth throws NoSuchElementException.
  • **count:** the same method works for my list (3) and for an ArrayList (2), because both are Iterable.

Iterable<?> means “an Iterable of anything”. It’s the way to write a method that accepts any traversable collection regardless of the type of its elements.

Questions you might ask

**Why is LinkedIterator an inner class and not static?** Because it needs to see the first field of the list that created it (private Node current = first;). An inner class (without static) belongs to a specific object and accesses its fields; a static one doesn’t.

**Why do two nested for-each loops work here and not for me?** Because iterator() returns a new LinkedIterator() every time. If I had stored a single iterator in an attribute, both loops would share a cursor, and the inner one would always leave it exhausted. (That’s exactly what the anecdote at the start is about.)

**Why does next() check current == null if hasNext() already exists?** Because nobody forces you to call hasNext() first. The contract says next() must throw NoSuchElementException when there are no elements left, and that’s what it does.

**And remove()?** We haven’t implemented it. Since it’s an optional operation in Iterator (by default it throws UnsupportedOperationException), our iterator is read-only. To support it we’d have to keep the previous node too and relink, as in the Python Tutor program.

Forgetting implements Iterable<T> on the class. If you only write an iterator() method but don’t declare that you implement Iterable, the for-each doesn’t compile: you’ll check this in the exercises.

Try it yourself

  1. Add to SinglyLinkedList an exists(T data) method that uses a for-each to say whether the data is in the list.
  2. Make the iterator support remove(): keep the previous node and relink. Start with the easy case (deleting a node that isn’t the first).

Program 3: ListIterator in both directions

The playlist is back. This time we walk it from start to end and from end to start with the same iterator, and modify it on the fly with set() and add().

The idea

ListIterator has a cursor that moves between two elements and can go forwards (next) and backwards (previous). We’ll try three things: going back and forth while showing positions with nextIndex() and previousIndex(); modifying with set() (replacing the returned song) and add() (inserting a new one); and reusing the same iterator, which ends at the end, to traverse in reverse.

add() inserts before the element next() would return, that is, right after the song you’ve just passed. That’s why the next next() keeps returning the usual song and not the new one.

The code

Use the same Song.java from program 1.

Both.java

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

public class Both {
    public static void main(String[] args) {
        List<Song> list = new ArrayList<>(List.of(
                new Song("Dawn", 215), new Song("Highway", 180),
                new Song("Storm", 240), new Song("Lighthouse", 135)));

        // 1. Back and forth with the same iterator
        ListIterator<Song> it = list.listIterator();
        System.out.println("Forwards:");
        while (it.hasNext()) {
            int i = it.nextIndex();
            System.out.println("  " + i + ": " + it.next());
        }
        System.out.println("Backwards:");
        while (it.hasPrevious()) {
            int i = it.previousIndex();
            System.out.println("  " + i + ": " + it.previous());
        }

        // 2. set() and add() while traversing
        it = list.listIterator();
        while (it.hasNext()) {
            Song c = it.next();
            if (c.title().equals("Highway")) {
                it.add(new Song("Interlude", 30));
            } else if (c.title().equals("Storm")) {
                it.set(new Song("Storm (remix)", 250));
            }
        }
        System.out.println("Result: " + list);

        // 3. The same iterator, now from the end
        System.out.print("From the end to the start:");
        while (it.hasPrevious()) {
            System.out.print(" " + it.previous().title() + ";");
        }
        System.out.println();
    }
}

Output

Forwards:
  0: Dawn (3:35)
  1: Highway (3:00)
  2: Storm (4:00)
  3: Lighthouse (2:15)
Backwards:
  3: Lighthouse (2:15)
  2: Storm (4:00)
  1: Highway (3:00)
  0: Dawn (3:35)
Result: [Dawn (3:35), Highway (3:00), Interlude (0:30), Storm (remix) (4:10), Lighthouse (2:15)]
From the end to the start: Lighthouse; Storm (remix); Interlude; Highway; Dawn;

Check it by hand:

  • Forwards: positions 0 to 3, with nextIndex() before each next().
  • Backwards: from 3 to 0. Here previousIndex() is used before each previous(): it returns the position of the element previous() is about to hand over.
  • Result: Interlude (0:30) has been inserted right after Highway, and Storm has been replaced by Storm (remix) (4:10). The rest stays the same.
  • From the end to the start: the iterator from the second part finished with the cursor at the end, so hasPrevious() is true and previous() hands over the songs starting from the last.

Questions you might ask

**Why doesn’t the program visit the new song again after an add()?** Because add() puts the cursor after the inserted element. The next next() returns Storm, not Interlude. That’s why there’s no risk of an infinite loop.

**Which song does set() affect?** The last one returned by next() or previous(). That’s why it goes right after the call that returned it. Here, Storm is the one returned by next() when the if recognises it.

**Why do nextIndex() and previousIndex() go before next() and previous()? Because they indicate the position of what is going to be** returned. If you call them afterwards, they give you the position of the next element, not the one you’ve just received.

**Can I use add() or set() with a plain Iterator?** No: only ListIterator has them. The simple Iterator allows remove() at most.

Calling set() or remove() right after an add(), or two set() calls in a row after an add(). After an add() there is no “last returned” and IllegalStateException is thrown.

Try it yourself

  1. Traverse the playlist backwards from the end (list.listIterator(list.size())) and use remove() to delete the songs shorter than 150 seconds.
  2. Insert an Interlude before each song instead of after. Hint: what do you have to do with previous()?

Common mistakes in this practice

  • **Deleting with list.remove(...) inside a for-each**: ConcurrentModificationException and a half-done list. Use it.remove().
  • **Calling it.remove() without a previous next()** (or twice in a row): IllegalStateException.
  • **Forgetting implements Iterable<T>** when writing a collection of your own: the for-each doesn’t compile.
  • Storing the iterator in an attribute and always returning the same one from iterator(): the second traversal finds nothing.
  • Confusing the position of the cursor with that of the element: nextIndex() is the one that is about to come out with next(), not the one that has just come out.
  • **Using add() and expecting the next next() to return what was added**: the cursor is already past it.

Watch it step by step in Python Tutor

Python Tutor runs the code line by line and draws variables, objects and references. Choose Java. Here you have the deletion of a node in a linked list, which is what remove() does inside: there’s no “gap” to patch, just an arrow that gets diverted.

public class Tutor {
    static class Node {
        String data;
        Node nextNode;

        Node(String data, Node nextNode) {
            this.data = data;
            this.nextNode = nextNode;
        }
    }

    public static void main(String[] args) {
        Node list = new Node("FP2", new Node("IC2", new Node("AEDV", null)));
        Node previous = null;
        Node current = list;
        while (current != null) {
            if (current.data.equals("IC2")) {
                previous.nextNode = current.nextNode;   // we skip the node: this is how an iterator "deletes"
            } else {
                previous = current;
            }
            current = current.nextNode;
        }
        for (Node n = list; n != null; n = n.nextNode) {
            System.out.println(n.data);
        }
    }
}

Expected output:

FP2
AEDV

What to watch, step by step:

  1. Before the while, you’ll see three Node boxes chained by arrows: FP2 → IC2 → AEDV. list points to the first, current does too, and previous is null.
  2. On the first iteration, FP2 is not IC2, so the else runs: previous now points to FP2 and current advances to IC2.
  3. On the second iteration the if holds. Look at the nextNode arrow of FP2: it **jumps over IC2** and points straight to AEDV. The IC2 box is still in the drawing, but nothing in the list reaches it any more.
  4. current = current.nextNode can still advance from IC2 (it keeps its arrow to AEDV). That’s why the traversal isn’t lost. In the end, the for prints only FP2 and AEDV.

Summary

  • The **for-each** asks an Iterable for an iterator and repeats hasNext() / next().
  • To delete while you traverse, use it.remove() (once per next()); with list.remove() you get ConcurrentModificationException and a half-done list.
  • removeIf is the shortcut for deleting by condition.
  • A collection of your own needs implements Iterable<T> and an iterator() that returns a new iterator with its own cursor.
  • An inner class (without static) is the natural way to write that iterator, because it sees the collection’s fields.
  • ListIterator adds previous(), nextIndex(), previousIndex(), set() and add(), and its cursor is always between two elements.

Checklist: have you practised it?

  • [ ] I compiled and ran the three programs.
  • [ ] I checked at least one output of each program by hand.
  • [ ] I saw the ConcurrentModificationException and the state it leaves the list in.
  • [ ] I traversed my SinglyLinkedList with two nested for-each loops.
  • [ ] I inserted and replaced songs with a ListIterator.
  • [ ] I looked at the Python Tutor program and saw an arrow skip over a node.

And now, on to the exercises: iterators exercises, with three levels, three fragments to debug and a cheat sheet at the end. See you there!

Next step: exercisesIterators in Java — exercises with playlists, repeated titles and a range with a step (and the “Lighthouse” that never left)

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *