跪拜 Guibai
← All articles
Kotlin · Android

Kotlin's Iterator and Sequence: Why Lazy Isn't Always Faster

By RockByte ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Sequence is often pitched as the performant choice, but its real win is avoiding intermediate collections and enabling short-circuiting on large or infinite data. On small lists, the wrapper overhead can make it slower than eager operations, so the decision should be driven by data size and pipeline length, not habit.

Summary

Every Kotlin for-loop, map, and filter call rests on the Iterator interface and its two functions: hasNext() and next(). MutableIterator adds a remove() that safely deletes during traversal, which is why calling a collection's own remove inside a for-loop throws ConcurrentModificationException.

Sequence achieves lazy evaluation by wrapping successive operations into new Sequence objects that defer all work until a terminal operation like toList() or first() pulls elements through the chain. Each element passes through the full pipeline before the next is fetched, enabling short-circuiting with take() or first() and avoiding intermediate collections.

Lazy execution comes with overhead: extra Sequence and Iterator wrappers plus indirect function calls. For small datasets or trivial transformations, eager collection operations can be faster. The right question is whether the task benefits from avoiding intermediate results or early termination, not whether to blindly call asSequence().

Takeaways
Kotlin's for-loop is syntactic sugar over Iterator; the compiler expands it to iterator(), hasNext(), and next() calls.
Calling a collection's own remove inside a for-loop causes ConcurrentModificationException because the iterator is unaware of the structural change.
MutableIterator.remove() deletes the last element returned by next() and keeps the iterator's internal state consistent.
Sequence.map() and filter() do not process data immediately; they return new Sequence objects that wrap the upstream and defer work.
Terminal operations like toList(), count(), and first() trigger the actual element-by-element traversal through the entire Sequence chain.
take() and first() short-circuit Sequence processing, stopping upstream data fetching once enough elements are collected.
Sequence avoids intermediate collections but still requires state for operations like distinct or sorted.
For small collections with simple operations, eager collection chains can outperform Sequence due to lower wrapper and call overhead.
Conclusions

Sequence's lazy model is a pull-based system: the terminal operation drives everything by repeatedly calling next() up the chain, which is why short-circuiting works naturally.

The decorator-like layering of Sequence operations means each intermediate step adds an object allocation and an indirect function call, a cost that only pays off when it prevents large intermediate allocations or unnecessary work.

Many developers treat asSequence() as a performance flag, but the real heuristic is simpler: use it when the pipeline is long, the data is large, or you need early termination; skip it for trivial in-memory lists.

Concepts & terms
Iterator Pattern
A behavioral design pattern that provides a way to access elements of a collection sequentially without exposing its underlying representation. In Kotlin, the Iterator interface defines hasNext() and next() as the sole contract.
MutableIterator
A Kotlin iterator subtype that adds a remove() function, allowing safe deletion of the element most recently returned by next() during traversal. Calling remove() without a preceding next(), or twice consecutively, throws IllegalStateException.
Pull-based model (lazy evaluation)
An execution model where downstream consumers request data one element at a time from upstream producers. Kotlin's Sequence uses this model: terminal operations pull elements through the chain, and no work happens until a pull occurs.
Terminal operation
An operation on a Sequence that triggers actual computation and produces a non-Sequence result, such as toList(), count(), first(), or forEach(). Until a terminal operation is called, intermediate operations like map and filter only build a description of the pipeline.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗