skip to content

An `Iterator` is single-use while an `Iterable` can be traversed repeatedly. How does this distinction affect API design and the relationship between `Iterable` and `Sequence`?

level: seniorimportance: should knowfreq 35%

answer

  1. Iterator = one-shot; Iterable = factory of fresh iterators
  2. second loop over a consumed Iterator sees nothing
  3. return Iterable/List for re-iteration, not bare Iterator
  4. Sequence = same shape, lazy/pull-based intent
  5. constrainOnce() makes single-pass fail loudly

basics

~20 s

An Iterator is consumed once — after you walk it, it's exhausted. An Iterable can hand out a fresh iterator each time, so you can loop over it again and again. So expose Iterable (re-iterable) in APIs, not a bare Iterator.

solid answer

~40 s

`Iterator<T>` carries traversal state and is **one-shot**: once `hasNext()` returns false it stays exhausted. `Iterable<T>` is a *factory* — each `iterator()` call yields a fresh cursor, so it's **re-iterable**. This drives API design: return `Iterable<T>` (or `List`) when callers may iterate more than once; returning a bare `Iterator` (or an `Iterable` whose `iterator()` returns the same backing iterator) creates a one-shot trap where a second `for` loop silently sees nothing. `Sequence<T>` mirrors `Iterable` structurally (`iterator()` returning `Iterator`) but signals **lazy, pull-based, potentially one-shot** semantics — `generateSequence`/`asSequence()` chains are evaluated element-by-element and a constrained sequence can throw on re-iteration. Eager `Iterable` operators (`map`, `filter`) build intermediate lists; lazy `Sequence` operators don't. Choosing `Iterable` vs `Sequence` is choosing eager+re-iterable vs lazy+streaming.

code

kotlin · 3 lines
kotlin
val seq = generateSequence(1) { it + 1 } // infinite, lazy
val firstThreeEvens = seq.filter { it % 2 == 0 }.take(3).toList()
println(firstThreeEvens) // [2, 4, 6] — only as much computed as needed

go deeper

for a junior

Knows an iterator gets used up and a list can be looped again.

for a middle

Explains the factory nature of Iterable and that you shouldn't return raw iterators for repeat use.

for a senior

Articulates the one-shot Iterable trap, the eager-vs-lazy contrast with Sequence, and short-circuiting.

for a principal

Frames API contracts around re-iterability/laziness, chooses Iterable vs Sequence vs Flow for streaming, and makes single-pass sources fail loudly.

## One-shot vs re-iterable - **`Iterator<T>`** holds a cursor. After exhaustion it cannot be reset — it's **single-use**. - **`Iterable<T>`** is a *producer of iterators*: `for` calls `iterator()` afresh each time, so the same `Iterable` can be looped repeatedly and independently. ```kotlin val iter = listOf(1, 2, 3).iterator() for (x in iter) {} // exhausts it for (x in iter) print(x) // prints nothing — already consumed val iterable = listOf(1, 2, 3) for (x in iterable) {} // fresh iterator for (x in iterable) print(x) // 1 2 3 — fresh again ``` ## The API design rule Return **`Iterable<T>`** (or a concrete `List`/`Collection`) when the caller might iterate more than once, pass it around, or call multiple operators. Returning a bare `Iterator<T>`, or building an `Iterable` whose `iterator()` always returns the *same* underlying iterator, is the classic **one-shot Iterable trap**: ```kotlin // TRAP: looks Iterable but is single-use fun lines(reader: BufferedReader): Iterable<String> { val it = reader.lineSequence().iterator() return Iterable { it } // returns SAME iterator each call } ``` The second consumer gets an empty view. Fix: return something that can rebuild the source, or document/return a `Sequence`. ## Relationship to `Sequence` `Sequence<T>` is structurally the same shape: ```kotlin public interface Sequence<out T> { public operator fun iterator(): Iterator<T> } ``` The difference is **semantic intent**: | | `Iterable<T>` | `Sequence<T>` | |---|---|---| | Evaluation | eager — each operator materializes a new list | lazy — pull-based, per-element through the chain | | Re-iteration | normally yes (collections) | sometimes; `constrainOnce()`/single-pass sources throw on second pass | | Use when | in-memory collections, multiple passes | large/infinite/streaming, few elements survive filters | - `list.map { }.filter { }` (Iterable) creates an intermediate list after `map`. - `list.asSequence().map { }.filter { }.first()` (Sequence) processes elements one at a time and can short-circuit. - `generateSequence(seed) { next }` produces a potentially **infinite, one-shot** sequence. - `sequence { yield(x) }` builds a lazy sequence with the coroutine-based builder. ## Practical guidance - Public API returning collection data: prefer `List`/`Iterable` for safe repeat traversal. - Pipeline over many elements with early termination: convert to `Sequence`. - Never hand callers a raw `Iterator` expecting them to re-loop. - If a source is inherently single-pass (a network/file stream), make that explicit — return a `Sequence` and consider `constrainOnce()` so accidental re-iteration fails loudly instead of silently empty.

  • Why can returning `Iterable { sameIterator }` be a bug?
    Because every `iterator()` call returns the same already-stateful iterator. The first consumer exhausts it, so any later traversal sees an empty sequence — it looks re-iterable but isn't.
  • When does `Sequence` beat `Iterable` performance-wise?
    When chaining multiple operators over many elements with early termination (e.g., `filter { }.map { }.first { }`). Sequences are lazy and avoid intermediate lists; for small collections or single operations, eager `Iterable` is usually faster due to less overhead.

An Iterable is a turnstile that issues a fresh ticket (iterator) on demand; an Iterator is the single used ticket — once stamped through, it's spent.

saying these in an interview costs you the question

  • Believing an `Iterator` can be looped twice
  • Returning a bare `Iterator` from a public API meant for repeated use
  • Thinking `Sequence` and `Iterable` are interchangeable with no semantic difference
  • Assuming `Sequence` is always faster (overhead loses on small/eager cases)
  • Not knowing `generateSequence`/constrained sequences can be one-shot

context