1. Overview
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.
2. Understanding Sequences
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.
3. Creating a Sequence
3.1. From Elements
To create sequence from elements, we just use the sequenceOf() function:
val seqOfElements = sequenceOf("first" ,"second", "third")
3.2. From a Function
To create an infinite sequence, we can call the generateSequence() function:
val seqFromFunction = generateSequence(Instant.now()) {it.plusSeconds(1)}
3.3. From Chunks
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.
3.4. From a Collection
To create a sequence from collections of Iterable interface, we should use the asSequence() function:
val seqFromIterable = (1..10).asSequence()
4. Lazy and Eager Processing
Let's compare two implementations. The first one, without a sequence, is eager:
val withoutSequence = (1..10).filter{it % 2 == 1}.map { it * 2 }.toList()
And the second, with a sequence, is lazy:
val withSequence = (1..10).asSequence().filter{it % 2 == 1}.map { it * 2 }.toList()
How many intermediate collections were introduced in each case?
In the first example, each operator introduces an intermediate collection. So, all ten elements pass to a map() function. In the second example, there are no intermediate collections introduced, thus map() function has only five elements as input.
5. Conclusion
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.
All code examples are available over on GitHub.