Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: March 19, 2024
The Kotlin language introduces sequences as a way to work with collections. They are quite similar to Java Streams, however, they use different key concepts under-the-hood. In this tutorial, we’ll briefly discuss what sequences are and why we need them.
A sequence is a container Sequence<T> with type T. It’s also an interface, including intermediate operations like map() and filter(), as well as terminal operations like count() and find().
Like Streams in Java, Sequences in Kotlin execute lazily. The difference is, if we use a sequence to process a collection using several operations, we won’t get an intermediate result at the end of each step. Thus, we won’t introduce a new collection after processing each step.
It has tremendous potential to boost application performance while working with large collections. On the other hand, there is an overhead to sequences when processing small collections.
To create sequence from elements, we just use the sequenceOf() function:
val seqOfElements = sequenceOf("first" ,"second", "third")
To create an infinite sequence, we can call the generateSequence() function:
val seqFromFunction = generateSequence(Instant.now()) {it.plusSeconds(1)}
We can also create a sequence from chunks with arbitrary length. Let’s see an example using yield(), which takes a single element, and yieldAll(), which takes a collection:
val seqFromChunks = sequence {
yield(1)
yieldAll((2..5).toList())
}
It’s worth mentioning here that all chunks produce elements one after another. In other words, if we have an infinite collection generator, we should put it at the end.
To create a sequence from collections of Iterable interface, we should use the asSequence() function:
val seqFromIterable = (1..10).asSequence()
Let’s compare two implementations. The first one, without a sequence, is eager:
val withoutSequence = (1..10).filter{it % 2 == 1}.map { it * 2 }
And the second, with a sequence, is lazy:
val withSequence = (1..10).asSequence().filter{it % 2 == 1}.map { it * 2 }.toList()
In the first example, each operator introduces an intermediate collection. All filtered elements are organized in a new List and passed to a map() function:
val list = (0..10)
assert(list is IntRange)
val filtered = list.filter { it % 2 == 1 }
assert(filtered is List<Int>)
val mapped = filtered.map { it * 2 }
assert(mapped is List<Int>)
assert(mapped.size == 5)
Also, we don’t need to use toList() as the map returns a List.
In the second example, no intermediate collections are introduced using a Sequence. The pipeline behaves similarly to the previous example, and the map takes all the filtered elements. In the end, calling toList() converts the Sequence to List. We can understand it better from the following code:
val sequence = (0..10).asSequence()
assert(sequence is Sequence)
val filtered = sequence.filter { it % 2 == 1 }
assert(filtered is Sequence)
val mapped = filtered.map { it * 2 }
assert(mapped is Sequence)
val list = mapped.toList()
assert(list is List<Int>)
assert(list.size == 5)
However, please note that all the operations in the pipeline are deferred and triggered by a terminal operation, which in this case is toList.
The main confusion with using these methods is that Collections are extended with convenience methods that have the same names as the methods in Sequence and also allow chaining. This way, it’s easy to mistake a simple method invocation chain with a proper pipeline.
In this tutorial, we briefly discussed sequences in Kotlin. We’ve seen how to create a sequence in different ways. Also, we’ve seen the difference in processing a collection with sequence and without it.