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.
Table of Contents
How to use this article
- Read the statement of a level and don’t look at the solution yet.
- Write your version, compile it and compare your output with the expected one.
- If you get stuck, look only at the 🔵 hint and try again.
- 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 inc. 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 % 60and printed with%02d, so that 10 is10and 5 is05.
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 returnsfalseif it was already there. The!turns “it was already there” into “delete it”.it.remove()deletes the last song thatnext()returned, so the next element isn’t skipped even if there are consecutive repeats (exactly the bug of the index loop in the anecdote).removedcounts the deletions: it’s 3 becauseLighthouseappears 3 times (removes 2) andTide2 (removes 1).- The list is
new ArrayList<>(List.of(...)). With a bareList.of,it.remove()would throwUnsupportedOperationException.
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 == 0and throwsIllegalArgumentException: 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. Withstart == endneither 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.currentis initialised tostart, so each call toiterator()starts from scratch. next()checkshasNext()and throwsNoSuchElementExceptionwhen no numbers are left: that’s what theIteratorcontract promises.- The
for (int n : new Range(...))works becauseRangeisIterable<Integer>, and Java converts eachIntegerinto anintautomatically. - 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:
- In the first iteration,
it.hasNext()istrueand the cursor’spositionvariable is 0. Aftertitle = it.next(), it’s 1 andtitleis"Dawn". - The second call,
second = it.next(), leavespositionat 2 andsecondis"Highway": the iteration has consumed two titles.Dawn / Highwayis printed. - In the second iteration,
hasNext()is stilltrue(2 < 3), but the first call tonext()already leavespositionat 3. When you enter the second call, theifchecks thatposition >= data.lengthand it’s true. - The
throwruns: the program stops on that line andsecondnever 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 withiterator(). Thefor-eachonly requires that. - **One
next()per iteration**, kept in a variable. - **Deleting while you traverse =
it.remove()**, neverlist.remove(). - **An
Iterableof your own returns a new** iterator on every call toiterator(). - **
ListIterator**: only on lists; addsprevious(),set()andadd().
Syntax
| I want to… | Code | Result |
|---|---|---|
| Traverse without indexes | for (Song s : list) { … } | Works with any Iterable |
| Ask for an iterator | Iterator<Song> it = list.iterator(); | New cursor, at the beginning |
| Traverse by hand | while (it.hasNext()) { Song s = it.next(); … } | One next() per iteration |
| Delete while traversing | it.remove(); | After next(), once per next() |
| Delete by a condition | list.removeIf(s -> s.seconds() < 150); | Shortcut |
| Make my class iterable | class A implements Iterable<T> { … } | And an iterator() method |
| Iterator of my own | return new IteratorA(); | Inner class with its cursor |
| Go backwards | ListIterator<T> it = list.listIterator(); it.previous(); | Lists only |
| Replace or insert | it.set(x); / it.add(x); | Acts at the cursor position |
| Accept any collection | static 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
| Symptom | Likely cause |
|---|---|
ConcurrentModificationException | You modify the list (remove/add) outside the iterator while traversing it |
| An element is left unvisited after deleting, with no exception | Deleted with list.remove() instead of it.remove() (happens with the second-to-last) |
NoSuchElementException | next() without hasNext(), or two next() per iteration |
IllegalStateException when deleting | remove() before the first next(), or two in a row |
UnsupportedOperationException when deleting | Immutable 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 nothing | iterator() always returns the same iterator |
Before you hand it in, ask yourself
- Do I make **a single
next()** per iteration and keep the result in a variable? - Do I delete with **
it.remove()** and not with the list’sremove? - Does my class declare **
implements Iterable<T>and return a new** iterator on everyiterator()? - 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) articlesDid this article help you? Here is how you can support SergioLearns →
