Kotlin's Iterator and Sequence: Why Lazy Isn't Always Faster
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.
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().
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.