Kotlin Collections: Iterators, Ranges, and Sequences

Iterators

Kotlin provides standard iterator mechanisms for traversing collection elements. An iterator is an object that grants sequential access to elements without exposing the underlying collection structure. This proves invaluable when processing each element individually, such as printing values or applying transformations.

Any class extending Iterable<T>, including Set and List, obtains an iterator via the iterator() function. Upon creation, the iterator positions before the first element. Calling next() returns the current element and advances the iterator to the subsequent position. Once the iterator passes the final element, it cannot retrieve additional elements nor reset to prior positions—create a fresh iterator to traverse again.

val items = listOf("apple", "banana", "cherry", "date")
val iterator = items.iterator()
while (iterator.hasNext()) {
    println(iterator.next())
}

The familiar for loop offers equivalent functionality, internally leveraging the iterator:

val items = listOf("apple", "banana", "cherry", "date")
for (item in items) {
    println(item)
}

Alternatively, forEach() automates iteration:

val items = listOf("apple", "banana", "cherry", "date")
items.forEach {
    println(it)
}

ListIterator

Lists support ListIterator for bidirectional traversal—forward and backward. The hasPrevious() and previous() functions enable reverse iteration, while nextIndex() and previousIndex() provide index information.

val items = listOf("apple", "banana", "cherry", "date")
val listIterator = items.listIterator()
while (listIterator.hasNext()) {
    listIterator.next()
}
while (listIterator.hasPrevious()) {
    print("Index: ${listIterator.previousIndex()}")
    println(", value: ${listIterator.previous()}")
}

This bidirectional capability allows ListIterator to remain functional after reaching the end.

MutableIterator

For mutable collections, MutableIterator extends Iterator with a remove() function, enabling element deletion during iteration:

val mutableItems = mutableListOf("apple", "banana", "cherry", "date")
val mutableIterator = mutableItems.iterator()
mutableIterator.next()
mutableIterator.remove()
println("After removal: $mutableItems")

MutableListIterator additionally supports insertion and replacement:

val mutableItems = mutableListOf("apple", "date", "date")
val mutableListIterator = mutableItems.listIterator()
mutableListIterator.next()
mutableListIterator.add("banana")
mutableListIterator.next()
mutableListIterator.set("cherry")
println(mutableItems)

Ranges and Progressions

Kotlin creates ranges via the rangeTo() function from kotlin.ranges or the .. operator. These typically pair with in or !in operators.

if (number in 1..4) { // equivalent to 1 <= number && number <= 4
    print(number)
}

Integer ranges (IntRange, LongRange, CharRange) support iteration as arithmetic progressions, commonly used in loops:

for (i in 1..4) print(i)

Reverse iteration uses downTo:

for (i in 4 downTo 1) print(i)

Custom step values employ the step function:

for (i in 1..8 step 2) print(i) // outputs: 1357
println()
for (i in 8 downTo 1 step 2) print(i) // outputs: 8642

To exclude the end element, use until:

for (i in 1 until 10) { // i in [1, 10), excluding 10
    print(i)
}

Range Characteristics

Mathematically, a range defines a closed interval between two inclusive endpoints. Ranges work with comparable types that define an order. The primary operation contains appears as in and !in operators.

Creating a custom range uses rangeTo() on the start value:

val versionRange = Version(1, 11)..Version(1, 30)
println(Version(0, 9) in versionRange)
println(Version(1, 20) in versionRange)

Progressions

Integer ranges like Int, Long, and Char function as arithmetic progressions represented by IntProgression, LongProgression, and CharProgression.

A progression possesses three properties: first element, last element, and non-zero step. Each subsequent element equals the previous plus the step. This mirrors Java's index-based loops:

for (int i = first; i <= last; i += step) {
    // ...
}

When creating ranges via iteration, the progression's first and last become endpoints, with step defaulting to 1:

for (i in 1..10) print(i)

Explicit step values apply via the step function:

for (i in 1..8 step 2) print(i)

The last element calculation differs by step direction:

  • Positive step: maximum value not exceeding the end where (last - first) % step == 0
  • Negative step: minimum value not below the end where (last - first) % step == 0

Consequently, the last element may not match the specified endpoint:

for (i in 1..9 step 3) print(i) // last element is 7

Reverse progressions use downTo:

for (i in 4 downTo 1) print(i)

Progressions implement Iterable<N> (where N is Int, Long, or Char), enabling use with collection functions:

println((1..10).filter { it % 2 == 0 })

Sequences

Beyond collections, Kotlin provides Sequence<T>, another container type offering similar functions to Iterable but with different processing semantics.

With Iterable, multi-step processing executes eagerly: each stage completes and produces an intermediate collection, then the next stage processes that collection. Sequences employ lazy evaluation: actual computation occurs only when the entire processing chain's result is requested.

Additionally, execution order differs: Sequence processes each element through all stages before moving to the next, whereas Iterable completes each stage for all elements before proceeding.

This lazy approach avoids intermediate results, improving performance for large processing chains. However, the overhead may impact smaller collections or simpler computations. Choose between Sequence and Iterable based on specific use cases.

Sequence Construction

From Elements

Create sequences via sequenceOf():

val wordsSequence = sequenceOf("four", "three", "two", "one")

From Iterable

Convert existing Iterable objects (like List or Set) using asSequence():

val items = listOf("one", "two", "three", "four")
val itemsSequence = items.asSequence()

From Function

Build sequences using element-generating functions via generateSequence(). Optionally specify the first element explicitly or as a function result. Sequence generasion stops when the function returns null, creating infinite sequences:

val oddNumbers = generateSequence(1) { it + 2 }
println(oddNumbers.take(5).toList())
// println(oddNumbers.count()) // Error: infinite sequence

Finite sequences require null return after the final element:

val oddNumbersBelowTen = generateSequence(1) { if (it < 10) it + 2 else null }
println(oddNumbersBelowTen.count())

From Chunks

The sequence() function generates elements individually or in chunks via a lambda containing yield() and yieldAll(). These return elements to the consumer and suspend execution until requested. yield() accepts a single element; yieldAll() accepts Iterable, Iterator, or another Sequence. Arguments to yieldAll() must be last—subsequent calls never execute:

val oddNumbers = sequence {
    yield(1)
    yieldAll(listOf(3, 5))
    yieldAll(generateSequence(7) { it + 2 })
}
println(oddNumbers.take(5).toList())

Sequence Operations

Sequance operations categorize by state requirements:

  • Stateless operations: Process each element independently without state—map() or filter(). Some stateless operations maintain small constant state, like take() and drop().
  • Stateful operations: Require substantial state proportional to sequence element count.

Operations returning another lazily-generated sequence are intermediate; otherwise they're terminal (e.g., toList() or sum()). Only terminal operations retrieve sequence elements.

Sequences support multiple iterations, though some implementations may restrict to single traversal—documentation clarifies this constraint.

Processing Comparison

The following demonstrates differences between Iterable and Sequence.

Iterable Example

Filtering words longer than three characters and collecting lengths of the first four:

val words = "The quick brown fox jumps over the lazy dog".split(" ")
val lengthsList = words.filter { println("filter: $it"); it.length > 3 }
        .map { println("length: ${it.length}"); it.length }.take(4)
println("Lengths of first 4 words longer than 3 chars:")
println(lengthsList)

Execution shows filter processing all elements first, then length mapping remaining filtered elements, then output—23 total steps.

Sequence Example

Equivalent processing using sequences:

val words = "The quick brown fox jumps over the lazy dog".split(" ")
val wordsSequence = words.asSequence()

val lengthsSequence = wordsSequence.filter { println("filter: $it"); it.length > 3 }
        .map { println("length: ${it.length}"); it.length }
        .take(4)

println("Lengths of first 4 words longer than 3 chars")
println(lengthsSequence.toList())

Output shows filter and map called only when building the result list. Processing occurs per-element: filter, then map, then check if more elements needed. Processing stops once take(4) reaches capacity—requiring only 18 steps.

Tags: kotlin Collections iterators Ranges Sequences

Posted on Sun, 10 May 2026 19:12:37 +0000 by Aérolithe