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 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 }

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.

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.