In the previous tutorial, you learned about delegation. Now let’s learn about sequences. Sequences are lazy collections that process elements one at a time instead of all at once. This can make a big difference in performance when working with large data sets.
In this tutorial, you will learn:
- Sequences vs Lists (lazy vs eager)
- Creating sequences
generateSequence- Sequence builders with
yieldandyieldAll - Infinite sequences
- When to use sequences
- Performance comparison
- Practical examples
Sequences vs Lists
Lists are eager — they process all elements at each step and create intermediate collections. Sequences are lazy — they process one element through all steps before moving to the next.
List: filter ALL -> map ALL -> take result
Sequence: take one -> filter it -> map it -> next one -> ...
Here is the difference in action:
// List (eager) — processes ALL elements at each step
val listResult = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.filter { println("List filter: $it"); it % 2 == 0 }
.map { println("List map: $it"); it * 10 }
.first()
Output: filters all 10, then maps all 5 evens, then takes the first.
// Sequence (lazy) — processes one element at a time
val seqResult = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.asSequence()
.filter { println("Seq filter: $it"); it % 2 == 0 }
.map { println("Seq map: $it"); it * 10 }
.first()
Output: filters 1 (fail), filters 2 (pass), maps 2, done! Only 2 elements processed instead of 10.
Both give the same result (20), but the sequence does much less work.
Creating Sequences
There are several ways to create a sequence.
From a Collection
val fromList = listOf(1, 2, 3, 4, 5).asSequence()
Using sequenceOf
val direct = sequenceOf(1, 2, 3, 4, 5)
Using generateSequence
val counting = generateSequence(1) { it + 1 }
.take(5)
.toList()
println(counting) // [1, 2, 3, 4, 5]
With Null Termination
Return null to stop the sequence:
val halving = generateSequence(100) { if (it > 1) it / 2 else null }
.toList()
println(halving) // [100, 50, 25, 12, 6, 3, 1]
Using sequence Builder
val custom = sequence {
yield(1)
yield(2)
yield(3)
}
println(custom.toList()) // [1, 2, 3]
generateSequence
generateSequence creates a sequence from a seed value and a function that produces the next value.
Fibonacci
val fibonacci = generateSequence(Pair(0, 1)) {
Pair(it.second, it.first + it.second)
}
.map { it.first }
.take(10)
.toList()
println(fibonacci) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Powers of 2
val powersOf2 = generateSequence(1) { it * 2 }
.take(10)
.toList()
println(powersOf2) // [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
Countdown
val countdown = generateSequence(10) { if (it > 0) it - 1 else null }
.toList()
println(countdown) // [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
Sequence Builders: yield and yieldAll
The sequence { } builder uses yield() to produce one value and yieldAll() to produce many values.
yield
val simple = sequence {
yield(1)
println("After 1")
yield(2)
println("After 2")
yield(3)
}
println(simple.first()) // 1
// "After 1" is NOT printed — the sequence is lazy!
The sequence stops as soon as first() gets what it needs.
yieldAll
val combined = sequence {
yield(0)
yieldAll(listOf(1, 2, 3))
yieldAll(generateSequence(4) { it + 1 })
}
println(combined.take(8).toList()) // [0, 1, 2, 3, 4, 5, 6, 7]
Flatten Nested Structures
data class Category(val name: String, val items: List<String>)
val categories = listOf(
Category("Fruits", listOf("Apple", "Banana")),
Category("Veggies", listOf("Carrot", "Pea")),
Category("Grains", listOf("Rice", "Wheat", "Oat"))
)
val allItems = sequence {
for (category in categories) {
yieldAll(category.items)
}
}
println(allItems.toList())
// [Apple, Banana, Carrot, Pea, Rice, Wheat, Oat]
Infinite Sequences
Sequences can be infinite. Just make sure you use take(), first(), or another terminal operation that limits the output.
Natural Numbers
val naturals = generateSequence(1) { it + 1 }
println(naturals.take(5).toList()) // [1, 2, 3, 4, 5]
Find Primes
fun isPrime(n: Int): Boolean {
if (n < 2) return false
for (i in 2..Math.sqrt(n.toDouble()).toInt()) {
if (n % i == 0) return false
}
return true
}
val firstPrimeAbove100 = generateSequence(101) { it + 1 }
.filter { isPrime(it) }
.first()
println(firstPrimeAbove100) // 101
val first10Primes = generateSequence(2) { it + 1 }
.filter { isPrime(it) }
.take(10)
.toList()
println(first10Primes) // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
The sequence only computes what is needed. It does not check every number up to infinity — it stops as soon as it finds enough primes.
When to Use Sequences
Use sequences when:
- The collection is large (thousands of elements or more)
- You chain multiple operations (filter, map, etc.)
- You only need some elements (
first(),take()) - You want to avoid intermediate collections
Use lists when:
- The collection is small
- You need random access (
list[index]) - You need the full result anyway
- You have a single operation
Example: Large Collection
val largeList = (1..1_000_000).toList()
// List: creates 2 intermediate lists (1M and 500K elements)
val listResult = largeList
.filter { it % 2 == 0 }
.map { it * 2 }
.take(5)
// Sequence: processes only 10 elements total
val seqResult = largeList
.asSequence()
.filter { it % 2 == 0 }
.map { it * 2 }
.take(5)
.toList()
Both produce the same result, but the sequence uses much less memory and does much less work.
Performance Comparison
Here is a comparison with a million elements:
val data = (1..1_000_000).toList()
// List approach — creates intermediate collections
val listTime = measureTimeMillis {
data
.filter { it % 3 == 0 }
.map { it.toLong() * it }
.filter { it > 1000 }
.take(10)
}
// Sequence approach — processes lazily
val seqTime = measureTimeMillis {
data
.asSequence()
.filter { it % 3 == 0 }
.map { it.toLong() * it }
.filter { it > 1000 }
.take(10)
.toList()
}
println("List: ${listTime}ms")
println("Sequence: ${seqTime}ms")
The sequence version is faster because it only processes the few elements needed to get 10 results, while the list version processes all 1 million elements through each step.
Practical Examples
Process Lines (Like a File)
fun processLines(lines: List<String>): List<String> {
return lines.asSequence()
.filter { it.isNotBlank() }
.map { it.trim() }
.filter { !it.startsWith("#") } // Skip comments
.map { it.uppercase() }
.toList()
}
val lines = listOf(
"# This is a comment",
" hello world ",
"",
" kotlin is great ",
"# Another comment",
" sequences are lazy "
)
println(processLines(lines))
// [HELLO WORLD, KOTLIN IS GREAT, SEQUENCES ARE LAZY]
Paginated Data
Simulate fetching pages of data:
fun fetchPages(): Sequence<List<String>> {
var page = 0
return generateSequence {
if (page < 5) {
val data = listOf("Item ${page * 3 + 1}", "Item ${page * 3 + 2}", "Item ${page * 3 + 3}")
page++
data
} else null
}
}
val allData = fetchPages().flatten().toList()
println(allData) // [Item 1, Item 2, ..., Item 15]
Collatz Sequence
The Collatz sequence is a famous math problem. Start with any number. If even, divide by 2. If odd, multiply by 3 and add 1. The sequence always reaches 1.
fun collatzSequence(start: Int): List<Int> {
return generateSequence(start) { n ->
when {
n == 1 -> null
n % 2 == 0 -> n / 2
else -> 3 * n + 1
}
}.toList()
}
println(collatzSequence(6))
// [6, 3, 10, 5, 16, 8, 4, 2, 1]
// Find starting number (1-100) with longest Collatz sequence
val longest = (1..100)
.asSequence()
.map { it to collatzSequence(it).size }
.maxByOrNull { it.second }
println("Start: ${longest?.first}, Length: ${longest?.second}")
Batch Processing
Process items in chunks:
fun processBatches(items: List<String>, batchSize: Int): List<String> {
return items.asSequence()
.chunked(batchSize)
.map { batch -> batch.map { it.uppercase() } }
.flatten()
.toList()
}
val items = (1..10).map { "item$it" }
println(processBatches(items, 3))
// [ITEM1, ITEM2, ITEM3, ITEM4, ITEM5, ITEM6, ITEM7, ITEM8, ITEM9, ITEM10]
This is useful when you need to process data in batches, like sending API requests or writing to a database.
Sequence Operations Reference
Here are the most common sequence operations:
Intermediate Operations (Lazy)
These return a new sequence and are not executed until a terminal operation is called:
val seq = (1..10).asSequence()
seq.filter { it > 5 } // Keep matching elements
seq.map { it * 2 } // Transform elements
seq.flatMap { listOf(it, it) } // Transform to collection and flatten
seq.take(3) // Take first N
seq.drop(3) // Skip first N
seq.distinct() // Remove duplicates
seq.sorted() // Sort (collects all elements!)
seq.zip(otherSeq) // Pair with another sequence
seq.chunked(3) // Group into chunks
Terminal Operations (Eager)
These trigger the computation and produce a result:
val seq = (1..10).asSequence()
seq.toList() // Convert to list
seq.toSet() // Convert to set
seq.first() // First element
seq.last() // Last element (processes all!)
seq.count() // Count elements
seq.sum() // Sum of numbers
seq.average() // Average of numbers
seq.min() // Minimum
seq.max() // Maximum
seq.any { it > 5 } // Any match?
seq.all { it > 0 } // All match?
seq.none { it < 0 } // None match?
seq.forEach { } // Execute for each
seq.joinToString() // Join to string
Warning: sorted() is an intermediate operation, but it must collect all elements to sort them. This removes the lazy benefit. If you need sorting, consider whether a list would be better.
Important Rules
- Sequences are lazy — nothing happens until you call a terminal operation (
toList(),first(),sum(),count(), etc.) - Call
toList()when you need the result as a list - Never iterate an infinite sequence without
take()orfirst() - Order matters — put
filterbeforemapto reduce work - Don’t overuse sequences — for small collections, lists are fine and simpler
Summary
| Concept | Example |
|---|---|
| Create from list | list.asSequence() |
| Create directly | sequenceOf(1, 2, 3) |
| Generate | generateSequence(1) { it + 1 } |
| Build | sequence { yield(1); yieldAll(list) } |
| Terminal operation | .toList(), .first(), .sum() |
| Intermediate operation | .filter(), .map(), .take() |
Key takeaways:
- Sequences are lazy, lists are eager
- Sequences process one element at a time through all steps
- Use sequences for large collections with multiple operations
- Use lists for small collections or when you need random access
- Always end a sequence chain with a terminal operation
- Never iterate an infinite sequence without
take()orfirst()
Source Code
You can find the complete source code for this tutorial on GitHub:
What’s Next?
Congratulations! You have completed the Intermediate series of the Kotlin Tutorial. You now know:
- Lambdas and higher-order functions
- Extension functions and properties
- Scope functions
- Sealed classes, enum classes, and value classes
- Interfaces, generics, and type constraints
- Error handling
- Delegation
- Sequences
With these skills, you are ready for more advanced topics. In the next tutorial, you will learn about coroutines — Kotlin’s way of writing asynchronous code without blocking threads.
This is part 16 of the Kotlin Tutorial series. Check out Part 15: Delegation if you missed it. Need a quick reference? See the Kotlin Cheat Sheet.