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

I was asked to remove the repeated titles from a list and I did it with an index for loop: if element i had already appeared before, remove(i) and carry on. With the sample data it worked beautifully. With the real data, where the same title showed up twice in a row, one always stayed behind. Java never complained. The problem was mine: when I deleted element i, the next one moved into its place and my i++ skipped it without looking.

Iterators exist, among other things, so that this doesn’t happen. Today we practise with three programs of increasing difficulty, three fragments to debug (one of them a cursor that overshoots) and a cheat sheet to take away.

These exercises use the iterators theory and review what’s in the practice. You can do them without those, but it will be harder.

How to use this article

  1. Read the statement of a level and don’t look at the solution yet.
  2. Write your version, compile it and compare your output with the expected one.
  3. If you get stuck, look only at the 🔵 hint and try again.
  4. When you finish, compare with the solution and pay attention to why it’s done that way, not just that it gives the same output.

No exercise uses Scanner. The data is fixed in the code so that your output is identical to the expected one and you can check it at a glance.

Level 1 · Basic: traversing a playlist with Iterator

Statement. Use this record to represent the songs (a record is a data class with its constructor and getters already made; title() and seconds() are its accessor methods):

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

In an N1 class with a main, create a list with these five songs:

  • Dawn, 215 seconds
  • Highway, 180 seconds
  • Storm, 240 seconds
  • Lighthouse, 135 seconds
  • Tide, 200 seconds

Traverse it with a **hand-written Iterator** (no for-each) and, for each song, print its order number and the song itself. When you finish, show the playlist’s total duration in m:ss format and which is the longest song.

Expected output:

1. Dawn (3:35)
2. Highway (3:00)
3. Storm (4:00)
4. Lighthouse (2:15)
5. Tide (3:20)
Total duration: 16:10
Longest: Storm (4:00)

Inside the while (it.hasNext()), a single call to next(), and keep the result in a variable. With it you can print it, add up its seconds and compare it with the longest so far. For the total duration, total / 60 is the minutes and total % 60 the seconds (with String.format("%02d", …) you always get two digits).

Level 2 · Intermediate: titles without repeats

Statement. You have this list of titles, with repetitions:

Lighthouse, Tide, Lighthouse, Dawn, Tide, Lighthouse

In N2, leave it with only the first appearance of each title, keeping the order, and count how many you removed. It has to be done with an Iterator and its remove(). Use an auxiliary HashSet<String> (a set: it doesn’t store repeats, and its add method returns false if the element was already there).

Print the list before, the list after and the number of titles removed.

Expected output:

Before:  [Lighthouse, Tide, Lighthouse, Dawn, Tide, Lighthouse]
After:   [Lighthouse, Tide, Dawn]
Removed: 3

For each title that next() gives you, try adding it to the set. If add returns false, you had already seen it: it.remove() and count one more. Remember to create the list with new ArrayList<>(List.of(...)), because List.of gives a list that can’t be modified.

Level 3 · Challenge: a range with a step

Statement. Create a Range class that is Iterable<Integer> and that generates the numbers from start up to end (this one not included) in steps of step. The step can be negative (countdown). It must be usable directly in a for-each:

for (int n : new Range(0, 10, 3)) { ... }   // 0 3 6 9

If the step is 0, the constructor throws IllegalArgumentException("the step can't be 0"). Each call to iterator() must give an independent traversal, and next() must throw NoSuchElementException when the numbers run out.

To test your class, use this main (class N3):

import java.util.List;

public class N3 {
    public static void main(String[] args) {
        System.out.print("From 0 to 10 in steps of 3:");
        for (int n : new Range(0, 10, 3)) {
            System.out.print(" " + n);
        }
        System.out.println();

        System.out.print("From 10 to 0 in steps of 4:");
        for (int n : new Range(10, 0, -4)) {
            System.out.print(" " + n);
        }
        System.out.println();

        System.out.print("From 5 to 5:");
        for (int n : new Range(5, 5, 1)) {
            System.out.print(" " + n);
        }
        System.out.println(" (empty)");

        List<Song> list = List.of(
                new Song("Dawn", 215), new Song("Highway", 180),
                new Song("Storm", 240), new Song("Lighthouse", 135),
                new Song("Tide", 200));
        System.out.println("Even positions of the playlist:");
        for (int i : new Range(0, list.size(), 2)) {
            System.out.println("  " + i + ": " + list.get(i));
        }

        try {
            new Range(0, 5, 0);
        } catch (IllegalArgumentException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}

Expected output:

From 0 to 10 in steps of 3: 0 3 6 9
From 10 to 0 in steps of 4: 10 6 2
From 5 to 5: (empty)
Even positions of the playlist:
  0: Dawn (3:35)
  2: Storm (4:00)
  4: Tide (3:20)
Error: the step can't be 0

The iterator needs a single piece of data of its own, the current number, which starts at start. To know whether numbers remain, look at the sign of the step: with a positive step you continue while current < end; with a negative step, while current > end. And next() returns the current value before adding the step to it.

What’s wrong here?

Three fragments, each with one bug. Before reading the explanation, do two things: predict whether it compiles and, if it does, what it prints (or what happens when you run it). All three use the same Song record from level 1.

Fragment 1

F1.java

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

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

        Iterator<Song> it = list.iterator();
        while (it.hasNext()) {
            System.out.println(it.next().title() + " lasts " + it.next().seconds() + " s");
        }
    }
}

Think about it before moving on: does it compile? what does it print? I wanted to see each title with its duration.

Explanation. It compiles and starts fine, but what it prints is false and, on top of that, it ends badly:

Dawn lasts 180 s
Exception in thread "main" java.util.NoSuchElementException
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1052)
	at F1.main(F1.java:13)

In each iteration there are two calls to next(), and each one advances the cursor. In the first iteration, the first call returns Dawn and the second returns Highway: the program mixes the title of one song with the duration of the next (and 180 s isn’t Dawn‘s, it’s Highway‘s). In the second iteration, hasNext() sees that Lighthouse remains, but the first call to next() already takes it, and the second runs into the end of the list: NoSuchElementException.

Calling next() several times inside the same iteration of the loop. hasNext() only guarantees one more element, and each next() consumes one. With an odd number of elements, the exception is thrown as well.

Fix. A single call to next(), kept in a variable, and then you can use it as many times as you like:

Iterator<Song> it = list.iterator();
while (it.hasNext()) {
    Song c = it.next();          // a single call to next() per iteration
    System.out.println(c.title() + " lasts " + c.seconds() + " s");
}
Dawn lasts 215 s
Highway lasts 180 s
Lighthouse lasts 135 s

Golden rule of the iterator: **one next() for every hasNext()**. If you need the element more than once, keep it in a local variable.

Fragment 2

F2.java

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

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

        Iterator<Song> it = list.iterator();
        while (it.hasNext()) {
            Song c = it.next();
            System.out.println("Checking " + c.title());
            if (c.seconds() < 150) {
                list.remove(c);
            }
        }
        System.out.println("Left " + list.size() + ": " + list);
    }
}

Does it compile? what does it print? I want to check every song and delete those shorter than 150 seconds. Notice which one gets deleted and what happens to the one after it.

Explanation. It compiles and throws no exception. And yet something is missing from the output:

Checking Dawn
Checking Highway
Checking Lighthouse
Left 3: [Dawn (3:35), Highway (3:00), Tide (3:20)]

Tide is never checked. Lighthouse is deleted with list.remove(c) (through the list, not the iterator) while being the second-to-last song: the list goes from 4 to 3 elements and the iterator’s cursor is exactly 3. Then hasNext(), which only compares “is the cursor different from the size?”, answers false and the loop ends without calling next() again. And it’s next() that checks whether the list has changed, so **no ConcurrentModificationException is thrown**. The bug slips by on tiptoe: the program “works” and skips a song.

Modifying the list outside the iterator. Sometimes it blows up with an exception, and sometimes, like here, it leaves a wrong result without warning. The second is worse.

Fix. The deletion is done through the iterator:

Iterator<Song> it = list.iterator();
while (it.hasNext()) {
    Song c = it.next();
    System.out.println("Checking " + c.title());
    if (c.seconds() < 150) {
        it.remove();                // deletes through the iterator
    }
}
Checking Dawn
Checking Highway
Checking Lighthouse
Checking Tide
Left 3: [Dawn (3:35), Highway (3:00), Tide (3:20)]

With it.remove() all four songs get checked and what is deleted is the same as before. The difference is that now the traversal is correct, and not by chance.

Fragment 3

Playlist.java

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

public class Playlist {
    private final List<Song> songs = new ArrayList<>();

    public void add(Song c) {
        songs.add(c);
    }

    public Iterator<Song> iterator() {
        return songs.iterator();
    }
}

F3.java

public class F3 {
    public static void main(String[] args) {
        Playlist list = new Playlist();
        list.add(new Song("Dawn", 215));
        list.add(new Song("Lighthouse", 135));

        for (Song c : list) {
            System.out.println(c);
        }
    }
}

*Does it compile? what does it print? The class has an iterator() method, doesn’t it? So it should work for a for-each.*

Explanation. It doesn’t compile:

F3.java:7: error: for-each not applicable to expression type
        for (Song c : list) {
                      ^
  required: array or java.lang.Iterable
  found:    Playlist
1 error

The for-each doesn’t look for a method called iterator(): it requires the type to be Iterable (or an array). Having the method isn’t enough, you have to declare that you fulfil the contract with implements Iterable<Song>. Java checks the type, not the shape.

Writing the iterator() method but forgetting implements Iterable<T> in the class header. The message talks about “array or java.lang.Iterable”, and that’s exactly what’s missing.

Fix. Declare the interface (and mark the method with @Override, which warns you if you get the signature wrong):

public class Playlist implements Iterable<Song> {
    private final List<Song> songs = new ArrayList<>();

    public void add(Song c) {
        songs.add(c);
    }

    @Override
    public Iterator<Song> iterator() {
        return songs.iterator();
    }
}
Dawn (3:35)
Lighthouse (2:15)

Always put @Override on iterator(), hasNext() and next(). If you get a letter wrong, the compiler complains right away instead of leaving you with a method nobody calls.

Solutions

Look at this section only once you’ve tried the exercise. If your code is different but gives the same output and respects the contracts, it’s also a valid solution.

Level 1 solution

N1.java

import java.util.Iterator;
import java.util.List;

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

        Iterator<Song> it = list.iterator();
        int number = 0;
        int total = 0;
        Song longest = null;
        while (it.hasNext()) {
            Song c = it.next();
            number++;
            total += c.seconds();
            System.out.println(number + ". " + c);
            if (longest == null || c.seconds() > longest.seconds()) {
                longest = c;
            }
        }
        System.out.println("Total duration: " + total / 60 + ":" + String.format("%02d", total % 60));
        System.out.println("Longest: " + longest);
    }
}

Why it’s done this way:

  • A single it.next() per iteration, kept in c. With that variable we print, add up the seconds and compare.
  • longest == null || c.seconds() > longest.seconds(): in the first iteration there is no “longest so far”, so the first song takes that place; from then on, it only changes if the new one is longer.
  • number++ before printing, so that the numbering starts at 1.
  • The seconds of the total are calculated with total % 60 and printed with %02d, so that 10 is 10 and 5 is 05.

Level 2 solution

N2.java

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

public class N2 {
    public static void main(String[] args) {
        List<String> titles = new ArrayList<>(List.of(
                "Lighthouse", "Tide", "Lighthouse", "Dawn", "Tide", "Lighthouse"));
        System.out.println("Before:  " + titles);

        Set<String> seen = new HashSet<>();
        Iterator<String> it = titles.iterator();
        int removed = 0;
        while (it.hasNext()) {
            String t = it.next();
            if (!seen.add(t)) {       // add() returns false if it was already there
                it.remove();
                removed++;
            }
        }
        System.out.println("After:   " + titles);
        System.out.println("Removed: " + removed);
    }
}

Why it’s done this way:

  • seen.add(t) does two things at once: it adds the title and returns false if it was already there. The ! turns “it was already there” into “delete it”.
  • it.remove() deletes the last song that next() returned, so the next element isn’t skipped even if there are consecutive repeats (exactly the bug of the index loop in the anecdote).
  • removed counts the deletions: it’s 3 because Lighthouse appears 3 times (removes 2) and Tide 2 (removes 1).
  • The list is new ArrayList<>(List.of(...)). With a bare List.of, it.remove() would throw UnsupportedOperationException.

Level 3 solution

Range.java

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

public class Range implements Iterable<Integer> {
    private final int start;
    private final int end;
    private final int step;

    public Range(int start, int end, int step) {
        if (step == 0) {
            throw new IllegalArgumentException("the step can't be 0");
        }
        this.start = start;
        this.end = end;
        this.step = step;
    }

    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<>() {
            private int current = start;

            @Override
            public boolean hasNext() {
                return step > 0 ? current < end : current > end;
            }

            @Override
            public Integer next() {
                if (!hasNext()) {
                    throw new NoSuchElementException();
                }
                int value = current;
                current += step;
                return value;
            }
        };
    }
}

Why it’s done this way:

  • The constructor validates step == 0 and throws IllegalArgumentException: it’s better to fail when creating the range than to end up in an infinite loop later.
  • hasNext() checks the sign of the step with a ternary operator: step > 0 ? current < end : current > end. With start == end neither condition holds, and the range comes out empty.
  • Here the iterator is an anonymous class (new Iterator<>() { ... }): it does the same as the named inner class from the theory, but written in place because it’s only used once. current is initialised to start, so each call to iterator() starts from scratch.
  • next() checks hasNext() and throws NoSuchElementException when no numbers are left: that’s what the Iterator contract promises.
  • The for (int n : new Range(...)) works because Range is Iterable<Integer>, and Java converts each Integer into an int automatically.
  • In the playlist test, new Range(0, list.size(), 2) generates the indexes 0, 2 and 4: that’s how the range is reused to jump two at a time.

Watch it step by step in Python Tutor

The bug of fragment 1 is perfect for watching live, because it happens on a single line. Go to pythontutor.com/java.html, choose Java and paste this program. It’s the same bug, with a homemade iterator that throws the exception in plain sight:

public class Tutor {
    static class Cursor {
        private final String[] data;
        private int position = 0;

        Cursor(String[] data) { this.data = data; }

        boolean hasNext() { return position < data.length; }

        String next() {
            if (position >= data.length) {
                throw new IllegalStateException("no songs left");
            }
            return data[position++];
        }
    }

    public static void main(String[] args) {
        Cursor it = new Cursor(new String[]{"Dawn", "Highway", "Lighthouse"});
        while (it.hasNext()) {
            String title = it.next();
            String second = it.next();       // second call in the same iteration!
            System.out.println(title + " / " + second);
        }
    }
}

Expected output:

Dawn / Highway
Exception in thread "main" java.lang.IllegalStateException: no songs left
	at Tutor$Cursor.next(Tutor.java:12)
	at Tutor.main(Tutor.java:22)

What to watch, step by step:

  1. In the first iteration, it.hasNext() is true and the cursor’s position variable is 0. After title = it.next(), it’s 1 and title is "Dawn".
  2. The second call, second = it.next(), leaves position at 2 and second is "Highway": the iteration has consumed two titles. Dawn / Highway is printed.
  3. In the second iteration, hasNext() is still true (2 < 3), but the first call to next() already leaves position at 3. When you enter the second call, the if checks that position >= data.length and it’s true.
  4. The throw runs: the program stops on that line and second never gets a value. Compare it with fragment 1: it’s the same traversal, only here you see the cursor at every step.

Cheat sheet: Iterators

The essentials in 30 seconds

  • **Iterator**: the cursor. hasNext() looks, next() advances and returns, remove() deletes the last one returned.
  • **Iterable**: something that knows how to give an iterator with iterator(). The for-each only requires that.
  • **One next() per iteration**, kept in a variable.
  • **Deleting while you traverse = it.remove()**, never list.remove().
  • **An Iterable of your own returns a new** iterator on every call to iterator().
  • **ListIterator**: only on lists; adds previous(), set() and add().

Syntax

I want to…CodeResult
Traverse without indexesfor (Song s : list) { … }Works with any Iterable
Ask for an iteratorIterator<Song> it = list.iterator();New cursor, at the beginning
Traverse by handwhile (it.hasNext()) { Song s = it.next(); … }One next() per iteration
Delete while traversingit.remove();After next(), once per next()
Delete by a conditionlist.removeIf(s -> s.seconds() < 150);Shortcut
Make my class iterableclass A implements Iterable<T> { … }And an iterator() method
Iterator of my ownreturn new IteratorA();Inner class with its cursor
Go backwardsListIterator<T> it = list.listIterator(); it.previous();Lists only
Replace or insertit.set(x); / it.add(x);Acts at the cursor position
Accept any collectionstatic int count(Iterable<?> c)Works for list, set…

Templates for the exercises

Manual traversal with an accumulator (Level 1):

Iterator<Song> it = list.iterator();
int total = 0;
while (it.hasNext()) {
    Song s = it.next();          // a single next() per iteration
    total += s.seconds();
}

Removing with an auxiliary set (Level 2):

Set<String> seen = new HashSet<>();
Iterator<String> it = titles.iterator();
while (it.hasNext()) {
    if (!seen.add(it.next())) {      // add() is false if it was already there
        it.remove();
    }
}

Iterable with an anonymous iterator (Level 3):

public class Range implements Iterable<Integer> {
    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<>() {
            private int current = start;         // cursor of its own

            @Override
            public boolean hasNext() { return step > 0 ? current < end : current > end; }

            @Override
            public Integer next() {
                if (!hasNext()) throw new NoSuchElementException();
                int value = current;
                current += step;
                return value;
            }
        };
    }
}

The mistakes that repeat the most

SymptomLikely cause
ConcurrentModificationExceptionYou modify the list (remove/add) outside the iterator while traversing it
An element is left unvisited after deleting, with no exceptionDeleted with list.remove() instead of it.remove() (happens with the second-to-last)
NoSuchElementExceptionnext() without hasNext(), or two next() per iteration
IllegalStateException when deletingremove() before the first next(), or two in a row
UnsupportedOperationException when deletingImmutable list (List.of) or an iterator without remove()
“for-each not applicable to expression type”The class doesn’t declare implements Iterable<T>
The second for-each traverses nothingiterator() always returns the same iterator

Before you hand it in, ask yourself

  1. Do I make **a single next()** per iteration and keep the result in a variable?
  2. Do I delete with **it.remove()** and not with the list’s remove?
  3. Does my class declare **implements Iterable<T> and return a new** iterator on every iterator()?
  4. Does my next() throw **NoSuchElementException** when no elements are left?

If you want to review the topic from the basics, go back to the iterators theory.

Keep goingAll Java (en) articles

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

Sigue aprendiendo

Leave a Reply

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