Iterator
Core. Expect to meet this one, and expect to be asked for it by name.
Start with the problem
A social feed keeps growing while a caller pages through it: newest post first, twenty at a time. The obvious way to fetch page two is to remember how many posts were already seen and skip that many.
public List<Entry> offsetPage(int offset, int size) {
int from = Math.min(offset, entries.size());
int to = Math.min(from + size, entries.size());
return List.copyOf(entries.subList(from, to));
}
For a feed that never changes mid-read, this walks the list correctly, one window at a time.
Watch where it goes
A new post lands at the front while a caller is still paging. entries shift by one position, so offset 2 no longer points at the same entry it pointed at before the insert. The caller asks for "whatever comes after what I already saw." offsetPage instead hands back an entry the caller has already read, because it was only ever remembering a position, not an entry.
This corpus's own demo shows the failure directly. Three posts sit in the feed as a, b, c. Reading by offset, offsetPage(0, 2) returns [c, b]. A fourth post, d, lands at the front. The next call, offsetPage(2, 2), returns [b, a]: b comes back a second time, because position 2 now points at a different entry than it did before the insert. Nothing threw. The method answered cleanly and reported something the caller had already seen.
The move
Anchor a page to an entry's own identity instead of to a position in whatever the collection happens to look like right now.
public Page page(Long cursor, int size) {
int start = 0;
if (cursor != null) {
while (start < entries.size() && entries.get(start).seq() >= cursor) {
start++;
}
}
int end = Math.min(start + size, entries.size());
List<Entry> window = List.copyOf(entries.subList(start, end));
boolean hasMore = end < entries.size();
Long nextCursor = hasMore ? window.get(window.size() - 1).seq() : null;
return new Page(window, nextCursor, hasMore);
}
This is corpus/shopping-cart's sibling lesson on Feed. cursor names a seq, which never moves once assigned, rather than naming a position, which moves every time something is inserted ahead of it. Reading the same three-post feed by cursor, page(null, 2) returns [c, b] with nextCursor=2. After d lands, page(2, 2) returns exactly [a], no repeat and no gap, because the cursor still names the same entry it always did.
What modern Java changes here
The question this lesson is really asking, decoupling how a caller walks a collection from how that collection stores its elements, is the same question java.util.Iterator answers inside a single process. A type that wants a for-each loop to work on it implements Iterable<T>, handing back an Iterator<T> whose hasNext() and next() do the walking:
final class Feed implements Iterable<Entry> {
private final List<Entry> entries = new ArrayList<>();
@Override
public Iterator<Entry> iterator() {
return entries.iterator();
}
}
for (Entry entry : feed) {
System.out.println(entry.text());
}
That Iterator is a live, single-pass, in-process walk. It fails fast with a ConcurrentModificationException if the collection changes underneath it mid-walk, which is exactly the bug the cursor design above is built to avoid. A cursor is the right shape once a walk has to survive across separate calls, possibly on separate machines, with writes landing in between. Iterable is the right shape once a walk only has to survive a single in-process loop.
When naming it is wrong
A method that already returns everything it has, in one call, does not need either shape. A cart with a handful of line items has no reason to page or to hand back a live iterator instead of the List<LineItem> it already builds. Adding a cursor, or a custom Iterator, to a collection small enough to return whole buys ceremony with no caller who benefits from it.
The threshold: reach for a cursor once a walk must survive between calls while the underlying data can change. Reach for Iterable once a walk only needs to survive one loop, in one process, over data that will not grow past what fits in memory. A small, fixed collection returned as a plain List is not under-engineered. Building either seam for it is `over-engineered (premature interface)` under the Standard's D3 dimension.
Where this lives in the app
Syllabus item D4 works through Feed.page against Feed.offsetPage, with the three-post, one-insert scenario above measured directly. _index.json anchors Iterator here because the lesson is the same decoupling problem the pattern solves, though the code itself pages a cursor rather than implementing java.util.Iterable; the snippet above supplies that half.