A list stores elements. A sequence describes how to supply elements when somebody asks for them. That difference matters when a pipeline transforms many values but needs only a few results. A sequence can stop requesting input after the answer is complete, rather than create an intermediate list for every stage.
Start with the collection operations lesson. This guide develops its brief sequence introduction into an execution model you can trace. Sequences are ordinary synchronous Kotlin code; they do not create worker threads, perform asynchronous networking, or automatically turn a slow algorithm into a fast one.
A pipeline is a recipe until a terminal operation runs
Calling asSequence on a list creates a sequence view of that source. Applying filter or map builds another lazy stage. These calls do not normally visit the list yet. A terminal operation such as toList, sum, firstOrNull or count requests elements and drives the preceding stages.
For a list pipeline, filter normally finishes its whole input before map begins. A sequence usually passes one element through the relevant stages before asking for the next. This changes the order of visible side effects. Prefer pure transformation functions so that the result does not depend on whether you chose eager or lazy processing.
Think about the consumer first. If it wants two matching records, take(2) expresses that demand. Upstream filtering may still inspect more than two inputs to find them. Place inexpensive rejection checks before expensive transformations when that order preserves meaning. Never reorder stages merely for speed if the predicates depend on the transformed value.
Laziness does not remove every allocation
Map and filter can pass one element at a time. Sorting is different: to know the smallest element of an arbitrary finite input, the implementation must consider all elements. A sorted sequence therefore needs storage and cannot yield a meaningful globally sorted result from an endless source. Distinct must remember values already encountered, so its memory can grow too.
ToList deliberately materializes the final results. That is useful when you need indexed access, repeatable snapshots, or an API expecting a List. It also means a sequence ending in toList still allocates the result list. The potential saving is intermediate work and storage, not the final storage you explicitly requested.
For a tiny list and a simple transformation, lazy wrappers and iterator calls may cost more than they save. Choose the clearest version first, then measure a representative workload if performance matters. Include realistic input sizes and the actual terminal operation. Timing only the construction of a lazy recipe does not measure processing at all.
Repetition, changing sources and endless input
Many sequences can be traversed repeatedly. Repeating a terminal operation can repeat the transformations as well. Store a materialized result if an expensive computation should happen once. Some sequence sources are explicitly constrained to a single traversal; consult the source's contract rather than assuming universal repeatability.
An asSequence view is not an immutable snapshot of a mutable list. If the source changes before traversal, the eventual traversal can observe different content. Mutating a collection during iteration can also violate the iterator's rules. Make a list copy when you require a stable boundary between data acquisition and later processing.
GenerateSequence can describe unbounded input. Always pair an endless producer with a bounded consumer such as take or a stopping predicate. Count or toList over an infinite sequence never completes. In the sequence builder, yield supplies one value to the consumer, and execution resumes when another value is requested; this is still synchronous iteration rather than a coroutine launched on another dispatcher.
Review and official references
Trace the terminal operation before predicting output. Identify which stages are stateless, which retain data, and whether the input is finite and repeatable. This reasoning catches mistakes that a superficial claim about lazy speed cannot explain. The official sequence guide describes evaluation order, while the Sequence API documents traversal contracts and available operations.
Kotlin Sequences and Lazy Pipelines Examples
Each example shows the code, the output it produces and what each part does.
Watch the requested elements
The ready message appears first because constructing the recipe does not traverse the list. Five is never inspected: the consumer already obtained its two values. The logging is useful for this lesson, but a production transformation should usually return its result without changing unrelated state.
fun main() {
val pipeline = listOf(1, 2, 3, 4, 5).asSequence()
.filter { println("check:$it"); it % 2 == 0 }
.map { println("map:$it"); it * 10 }
.take(2)
println("ready")
println(pipeline.toList())
}ready
check:1
check:2
map:2
check:3
check:4
map:4
[20, 40]Kotlin Sequences and Lazy Pipelines in Real Projects
Worked exercise: Find the first valid stock codes
A supplier sends text codes. Keep the first three nonblank trimmed codes whose length is at most four. Preserve their order and return a list. The limit belongs after validation: limiting raw input first could discard later valid codes before they are examined.
fun firstCodes(lines: List<String>): List<String> = lines.asSequence()
.map { it.trim() }
.filter { it.isNotEmpty() && it.length <= 4 }
.take(3)
.toList()
fun main() {
val lines = listOf(" ", " PEN ", "TOO-LONG", "A4", "INK", "BOX")
println(firstCodes(lines))
println(firstCodes(emptyList()))
}[PEN, A4, INK]
[]The empty case returns an empty list without special handling. BOX is unnecessary once three accepted codes exist. If duplicate codes must be removed, place distinct before take, and recognize that remembering previous codes requires additional state. Tests should cover fewer than three valid inputs and invalid values at the beginning, middle and end.
Kotlin Sequences and Lazy Pipelines FAQ
Does a sequence run in parallel?
No. A standard Kotlin Sequence is pulled through its iterator by the caller, normally on the caller's current thread. Laziness controls when and how much work occurs, rather than which thread performs it. A map containing a blocking call still blocks its consumer. Use an appropriate coroutine or other concurrency API when you actually need asynchronous work, and keep that decision separate from choosing a lazy collection pipeline.
Why does printing a sequence not show all its elements?
A sequence is a producer abstraction, not a stored list of results. Printing its object representation does not generally demand every element. Use a suitable terminal operation, such as toList for a bounded sequence or joinToString when a finite textual result is wanted. Avoid casually materializing a potentially endless source just to inspect it; take a small number of elements first so debugging has a clear bound.
Should every collection pipeline use asSequence?
No. Small collections and simple transformations often benefit more from straightforward eager code than from laziness. Sequences are especially helpful when a chain would create substantial intermediate collections or when a short-circuiting consumer needs only part of the source. Compare equivalent completed computations with realistic data before claiming an improvement. Readability, source ownership and repeatability are also part of the choice, not just an isolated timing measurement.
Can I safely call two terminal operations on the same sequence?
That depends on the sequence source. A sequence backed by a list is usually repeatable, but its computations run again and a mutable source may have changed. A source documented as single-use can reject the second traversal. If two consumers need exactly the same results, materialize a bounded sequence once and use the resulting list. This makes the timing and memory cost explicit and avoids accidentally repeating external effects.
Kotlin Sequences and Lazy Pipelines Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
A sequence may be infinite. Which expression obtains a bounded sample?
Kotlin Sequences and Lazy Pipelines Quiz
2 questions to check what you learned.
What starts traversal of this pipeline: source.asSequence().filter { it > 0 }.toList()?
- asSequence always visits every element immediately.
- toList requests the pipeline's elements.
- filter starts a background thread.
Why can sorted() still need substantial memory on a sequence?
- Sequence guarantees every operation uses exactly one element of memory.
- Sorting only examines the first element.
- Global ordering requires examining and retaining finite input before producing sorted results.