Layers of Logic

10.2

Iterable and the Iterator

Implement and use Iterable and Iterator, trace how for-each obtains elements, and remove elements without corrupting traversal.

Core18 min read4 exercises

Before you start

Topics used in this lesson

01

Prerequisites

Section 10.1 showed you the hierarchy, with Iterable sitting above everything.

You have been using its one method since Section 4.1. Back there you were told the for-each loop is not really a loop, and that Phase X would explain it.

02

Why this matters

Walk through a collection and look at every element. You already know one way:

for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

That works for a List. It fails for everything else, in two different ways.

Most collections have no positions.

Set<String> names = new HashSet<>();
names.get(0);      // does not compile. A Set has no get.

A Set does not number its elements. Nor does a Map, or a Queue. Only List offers get(i), so this loop cannot be written for three quarters of the framework.

Where it does compile, it can be terrible.

LinkedList is a List, so it has get(i). But a linked list has no positions in memory. Each node only knows the next one, so get(5000) has to start at the front and follow 5000 references.

Loop over it by index and you do that on every single pass. Measured:

40,000 elementsTime
ArrayList, by indexrandom access, from 4.21 ms
ArrayList, for-each1 ms
LinkedList, by indexrestarts from the front each time806 ms
LinkedList, for-eachremembers where it isunder 1 ms

Double the size and the LinkedList index column goes up four times, not two. That shape is the signature of work growing with the square of the input.

03

Core explanation

What is needed is something that walks a collection without needing positions, and remembers where it got to.

That is an iterator, and Java describes it with two tiny interfaces.

public interface Iterable<T> {
    Iterator<T> iterator();
}

public interface Iterator<T> {
    boolean hasNext();
    T next();
}

Iterable says: I can give you something that walks me. Iterator says: I know where I am, and whether there is more.

That is the whole design. One method on the top interface of the entire framework.

The for-each loop is this, in disguise

for (String s : names) {
    System.out.println(s);
}

The compiler rewrites that into:

Iterator<String> it = names.iterator();
while (it.hasNext()) {
    String s = it.next();
    System.out.println(s);
}

Which explains everything the for-each does and does not do.

It works on anything that implements Iterable. A list, a set, a tree set, your own class. The same line, unchanged, because all of them can hand back an iterator.

It does not give you an index, because an iterator has no idea what position it is at. It only knows “here” and “is there more”.

04

Optional internals

Going deeper
Open optional internalsYou can finish the core lesson without this section.

Where the iterator lives

ArrayList does not implement Iterator itself. That would be wrong: an ArrayList would then have one shared position, and two loops over the same list would interfere.

Instead each call to iterator() returns a new object with its own cursor:

public Iterator<E> iterator() {
    return new Itr();
}

private class Itr implements Iterator<E> {
    int cursor;          // where we are
    // ...
}

Itr is an inner class, from Section 7.4. It is not static, so it holds a hidden reference to the list, which is how it reads the elements.

That design means two nested loops over one list each get their own cursor and do not collide.

The exception you will meet

List<String> names = new ArrayList<>(List.of("Atlas", "Beacon", "Cipher"));

for (String s : names) {
    if (s.startsWith("B")) names.remove(s);      // ConcurrentModificationException
}

The iterator keeps a count of how many times the list has been structurally changed. The list keeps the same count. When you call remove on the list directly, the list’s count goes up and the iterator’s does not.

On the next next(), the iterator compares them, finds they disagree, and throws.

Two correct ways to remove while walking

Use the iterator’s own remove(). It updates both counts, so they never disagree.

Iterator<String> it = names.iterator();
while (it.hasNext()) {
    if (it.next().startsWith("B")) {
        it.remove();
    }
}

Note that remove() deletes what next() just returned, so you must call next() first.

Or use removeIf, which is shorter and does the same thing underneath:

names.removeIf(s -> s.startsWith("B"));

removeIf is a default method on Collection, added in Java 8 by the mechanism from Section 8.5. It could be added because default methods let an interface grow without breaking every implementation.

That lambda is the Section 7.4 anonymous class with the noise removed, and Phase XI is about nothing else.

Making your own class work with for-each

Implement Iterable and your class joins in:

class Registry implements Iterable<String> {
    private final String[] names = {"Atlas", "Beacon", "Cipher"};

    @Override
    public Iterator<String> iterator() {
        return new Iterator<>() {
            private int cursor = 0;
            public boolean hasNext() { return cursor < names.length; }
            public String next() { return names[cursor++]; }
        };
    }
}

for (String name : new Registry()) {
    System.out.println(name);
}

One method, and the for-each loop works on your class exactly as it does on an ArrayList.

That anonymous class is Section 7.4 again, and it cannot be a lambda, because Iterator has two abstract methods rather than one.

05

Tradeoffs

An iterator gives you no index. If you need to know you are at position 7, a for-each cannot tell you, and you are back to a counter or an index loop.

It only goes forward. Iterator has no way back. List offers a ListIterator that can, and only lists have it.

Every call to iterator() also allocates an object. That costs nothing once. It adds up in code that loops over small collections millions of times.

The concurrent modification check costs a comparison on every next(), and as you saw, it still does not catch every case.

What you get is one line that walks anything. Change an ArrayList to a TreeSet and your loops keep working, unchanged, because both promise the same single method.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. Why can you not walk every collection with `for (int i = 0; i < c.size(); i++) c.get(i);`?

    Show the answer

    Two reasons, and either one is enough.

    Most collections have no get(i). A Set has no positions. Neither does a Map or a Queue. Only List offers indexed access, so the loop does not even compile for the rest.

    Even where it compiles, it can be terrible. A LinkedList has get(i), and every call walks from the front. Looping by index over 40,000 elements takes about 806 ms. The same walk with a for-each takes under 1 ms.

    An iterator fixes both. It does not need positions, and it remembers where it is instead of starting again.

  2. What does the compiler turn `for (String s : names)` into?

    Show the answer

    Into an iterator loop:

    Iterator<String> it = names.iterator(); then a while (it.hasNext()) loop whose first line is String s = it.next();

    That is the whole trick. Iterable has one method, iterator(), and anything that implements it can be used in a for-each.

    It is why the same line works on an ArrayList, a HashSet, a TreeSet and your own class. Arrays get the same treatment through a special case in the compiler, since an array is not really a class.

  3. Removing an element inside a for-each usually throws `ConcurrentModificationException`. Why is 'usually' the worrying word?

    Show the answer

    Because the check is not a guarantee. Removing the second to last element does not throw at all.

    The iterator checks for changes inside next(). Remove the second to last element and the size drops to match the cursor, so hasNext() returns false, the loop exits, and next() is never called again. No check, no exception, and you quietly skipped an element.

    Try it on [A, B, C, D]: removing A, B or D throws. Removing C does not.

    So the exception is a helpful warning, not a safety net. The fix is to use it.remove() or removeIf, both of which are always correct.

07

Exercises

Start with the warm-ups. Continue to practice exercises when the basic idea works. Challenges are optional: skip them on a first pass and return after the next lesson.

  1. Warm up: one idea with close guidance.
  2. Practice: combine the idea with earlier material.
  3. Challenge: investigate an edge case or implementation detail.

4 exercises90 pointsabout 100 minutes

Coding exercises provide starter code and concrete success checks. Exercises marked checked can also be run through the VS Code extension. Concept questions are selectable quizzes that give immediate feedback on this page.
A

Measure the 806 Milliseconds

Practice·25 min·20 points

checkedex-10-2-a

Measure why the iterator exists.

Twelve numbers, and three of them tell the story. Watch what happens to the LinkedList index column when you double the input: it does not double.

Then explain why the for-each column stays flat while the index column explodes. The difference is one thing: an iterator remembers where it is, and get(i) starts again from the front every time.

Finish by trying the index loop on a HashSet. It will not compile, and that is the second reason iterators had to be invented.

Required class and methods

Keep these names and parameter types. The checker uses them to find your code.

class WhyIterator {
    static long sumByIndex(List<Integer> l)
    static long sumByForEach(Iterable<Integer> l)
    static long timeMs(Runnable work)
}

sumByIndex must use get(i). sumByForEach must use a for-each loop, and takes Iterable so it also works on a Set, which has no index at all. timeMs runs the work and returns how many milliseconds it took.

Steps and success checks

  • Implement the three methods
  • Print all twelve numbers, three rows of four
  • Say what happens to the LinkedList index column when n doubles
  • Explain why the LinkedList for-each column stays flat
  • Call sumByForEach on a HashSet and say why sumByIndex cannot be
WhyIterator.java
import java.util.*;

public class WhyIterator {

    // Must use get(i). That is what makes it slow on a LinkedList.
    static long sumByIndex(List<Integer> l) {
        return 0; // TODO
    }

    // Must use a for-each loop. Iterable, not List, so a Set works too.
    static long sumByForEach(Iterable<Integer> l) {
        return 0; // TODO
    }

    // Run the work, return how many milliseconds it took.
    static long timeMs(Runnable work) {
        return 0; // TODO
    }

    public static void main(String[] args) {
        for (int n : new int[]{10_000, 20_000, 40_000}) {
            List<Integer> al = new ArrayList<>();
            List<Integer> ll = new LinkedList<>();
            for (int i = 0; i < n; i++) { al.add(i); ll.add(i); }

            // TODO: print all four timings for this n, using timeMs
        }
    }
}
Hint 1
timeMs is System.nanoTime() before and after, then divide the difference by 1_000_000.
Hint 2
Watch the LinkedList index column across the three rows. Doubling n does not double the time, it roughly quadruples it.
Hint 3almost the answer
Each get(i) starts at the front and follows i references. Doing that for every i means the total work grows with the square of n. The for-each loop keeps its place instead, so it stays flat.
Why this exercise exists

This exercise checks whether the reason iterators exist is a number you produced.

B

The Exception That Sometimes Does Not Fire

Practice·25 min·25 points

checkedex-10-2-b

Four removals from the same list. Three throw and one does not.

Find the one that does not, then work out why it is special. The answer is about which check runs and when, not about the value being removed.

That silent case is what makes this bug worse than a normal exception. A method that throws most of the time trains you to trust the exception as a safety net, and then one day it quietly skips an element instead.

Fix it both correct ways and say which you would put in real code.

Required class and methods

Keep these names and parameter types. The checker uses them to find your code.

class Cme {
    static boolean throwsWhenRemoving(String target)
    static List<String> removeWithIterator(List<String> l, String target)
    static List<String> removeWithRemoveIf(List<String> l, String target)
}

throwsWhenRemoving builds a fresh [A, B, C, D] every call, walks it with for-each, removes the target through the list, and reports whether ConcurrentModificationException came out. The two fixes remove every copy of the target and return the same list.

Steps and success checks

  • Implement the three methods
  • Run it and identify which removal does not throw
  • Explain why that one is different, using hasNext and the cursor
  • Say which of the two fixes you would ship, and why
Cme.java
import java.util.*;

public class Cme {

    // Build a fresh [A, B, C, D], walk it with for-each, remove target through the
    // LIST (not the iterator), and report whether the exception came out.
    static boolean throwsWhenRemoving(String target) {
        return true; // TODO
    }

    // Same job, done safely with an explicit Iterator.
    static List<String> removeWithIterator(List<String> l, String target) {
        return l; // TODO
    }

    // Same job again, in one line.
    static List<String> removeWithRemoveIf(List<String> l, String target) {
        return l; // TODO
    }

    public static void main(String[] args) {
        System.out.println("removing each element from [A, B, C, D]:");
        for (String t : new String[]{"A", "B", "C", "D"}) {
            // TODO: print t and whether it threw
        }

        // TODO: one of the four does NOT throw. Which one, and why?
    }
}
Hint 1
Catch ConcurrentModificationException around the loop and return true from the catch, false from the end of the try.
Hint 2
Three of the four throw. The one that does not is at a specific position, and working out which position tells you the mechanism.
Hint 3almost the answer
The check lives inside next(). Remove the second to last element and the size drops to match the cursor, so hasNext() is false, the loop ends, and next() is never called again. it.remove() updates both counts so they never disagree, and removeIf does the same thing in one line.
Why this exercise exists

This exercise checks whether you know ConcurrentModificationException is a warning rather than a guarantee.

C

Make Your Own Class Work With For-Each

Practice·30 min·25 points·The Registry

checkedex-10-2-c

One method, and your own class works with the same loop syntax as every collection in Java.

Write it, then run two nested loops over the same object at once. They should not interfere, and understanding why tells you something about where the cursor lives.

Then call forEach on your class. You never wrote it. Work out where it came from, and you will have connected this section back to default methods in Section 8.5.

Finish with the question about lambdas. Iterator looks like it should be one and cannot be, and the reason is a rule you already know.

Required class and methods

Keep these names and parameter types. The checker uses them to find your code.

class MyRegistry {
    public Iterator<String> iterator()
}

MyRegistry already declares implements Iterable. Write the one method it is missing. Past the last name, next() must throw NoSuchElementException rather than return null.

Steps and success checks

  • Implement iterator() so the for-each loop works
  • Run two nested loops over the same object and confirm they do not interfere
  • Call forEach on your class and say where that method came from
  • Explain why an Iterator cannot be a lambda
MyRegistry.java
import java.util.*;

public class MyRegistry implements Iterable<String> {

    private final String[] names = {"Atlas", "Beacon", "Cipher", "Drift"};

    @Override
    public Iterator<String> iterator() {
        return null; // TODO
    }

    public static void main(String[] args) {
        MyRegistry r = new MyRegistry();

        // TODO: this should work once iterator() is written
        // for (String name : r) System.out.println(name);

        // TODO: two nested loops over the same object. Do they interfere?
        // TODO: add a forEach call and note where that method came from
    }
}
Hint 1
Return an anonymous class from Section 7.4: return new Iterator<>() { int cursor = 0; ... }; with hasNext and next.
Hint 2
Nested loops work because each call to iterator() creates a new object with its own cursor. If your class itself held the cursor, the inner loop would use it up and the outer loop would stop after one turn.
Hint 3almost the answer
forEach is a default method on Iterable, added in Java 8. You inherited it for free by writing one method. An Iterator cannot be a lambda because it has two abstract methods, and a lambda needs exactly one.
Why this exercise exists

This exercise checks whether you can implement Iterable.

D

Walk Everything With One Method

Warm up·20 min·20 points

checkedex-10-2-d

Write the loop once and use it on everything.

Five collection types, one method, no changes. That is what the top of the hierarchy buys you.

Then find the two things that do not fit, and notice they fail for completely different reasons. An array is not a class. A Map is a class and is not Iterable.

Both failures are worth being able to explain, because both look like the same problem from the error message alone.

Required class and methods

Keep these names and parameter types. The checker uses them to find your code.

class WalkAll {
    static List<String> walk(Iterable<?> items)
}

Returns each element turned into a String, in the order the iterator gave them. Use String.valueOf so a null element does not crash the walk.

Steps and success checks

  • Write one method that walks any Iterable
  • Call it with five different collection types
  • Show why an array and a Map both fail, for different reasons
  • Make the Map work, three ways
WalkAll.java
import java.util.*;

public class WalkAll {

    // One method that walks anything with an iterator.
    static List<String> walk(Iterable<?> items) {
        return List.of(); // TODO
    }

    public static void main(String[] args) {
        // TODO: call it with an ArrayList, LinkedList, HashSet, TreeSet, ArrayDeque
        // TODO: try to call it with an int[]. Read the error, then comment it out
        // TODO: try to call it with a HashMap. Read that error too
        // TODO: make the map work, three ways
    }
}
Hint 1
Take Iterable<?> as the parameter. That is the top of the hierarchy, so every collection fits.
Hint 2
An array fails because it is not a class and cannot implement an interface. The for-each still works on arrays through a compiler special case, but you cannot pass one where an Iterable is expected.
Hint 3almost the answer
A Map fails for a different reason: it is not Iterable at all. Pass map.keySet(), map.values() or map.entrySet(), which are real collections.
Why this exercise exists

This exercise checks whether you can see that one loop covers the whole framework.

08

Where this leads

Iterable promises one thing: you can walk me. That is not much to build a framework on.

Section 10.3 is the next level down, where Collection adds the operations you actually use: add, remove, size, contains, isEmpty. Every list, set and queue in Java inherits all of them from one place.

You will also find out why Collection.remove(Object) returns a boolean while List.remove(int) returns the removed element. It is the overload trap from Section 7.3, waiting in a new place.

Further out, in Phase XI, stream() turns out to be another default method bolted onto Collection, exactly like removeIf and forEach. The lambda you passed to removeIf in this section is the first of thousands.

Later connections

Iterator is next used in 10.3 - The Collection Interface