10.2
Iterable and the Iterator
The for-each loop is not a loop. It is one interface with one method, and it is why the same line works on an array, a list, a set and a map.
Previously on
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.
The problem
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 elements | Time | |
|---|---|---|
| ArrayList, by index | random access, from 4.2 | 1 ms |
| ArrayList, for-each | 1 ms | |
| LinkedList, by index | restarts from the front each time | 806 ms |
| LinkedList, for-each | remembers where it is | under 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.
The idea
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”.
Under the hood
Going deeperWhere 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.
What it costs
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.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
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). ASethas no positions. Neither does aMapor aQueue. OnlyListoffers indexed access, so the loop does not even compile for the rest.Even where it compiles, it can be terrible. A
LinkedListhasget(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.
What does the compiler turn `for (String s : names)` into?
Show the answer
Into an iterator loop:
Iterator<String> it = names.iterator();then awhile (it.hasNext())loop whose first line isString s = it.next();That is the whole trick.
Iterablehas 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, aHashSet, aTreeSetand your own class. Arrays get the same treatment through a special case in the compiler, since an array is not really a class.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, sohasNext()returns false, the loop exits, andnext()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()orremoveIf, both of which are always correct.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises90 pointsabout 100 minutes
Measure the 806 Milliseconds
ex-10-2-aMeasure 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.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
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.
What your program must do
- 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
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
Hint 3almost the answer
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.The Exception That Sometimes Does Not Fire
ex-10-2-bFour 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.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
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.
What your program must do
- 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
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
ConcurrentModificationException around the loop and return true from the catch, false from the end of the try.Hint 2
Hint 3almost the answer
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.Make Your Own Class Work With For-Each
ex-10-2-cOne 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.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class MyRegistry {
public Iterator<String> iterator()
}MyRegistry already declares implements Iterable
What your program must do
- 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
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 new Iterator<>() { int cursor = 0; ... }; with hasNext and next.Hint 2
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.Walk Everything With One Method
ex-10-2-dWrite 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.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
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.
What your program must do
- 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
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
Iterable<?> as the parameter. That is the top of the hierarchy, so every collection fits.Hint 2
Hint 3almost the answer
map.keySet(), map.values() or map.entrySet(), which are real collections.After the credits
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.
Threads you opened in this section
- IteratorA Stream walks a source once, the same way, and cannot be reused.11.3 - Streams, and When the Work Happens
Iterator will return in 10.3 - The Collection Interface