One of the most common questions I get about Kotlin’s sequences is this:

“When should I use sequences, and when should I use normal collections?”

That’s the question we’re going to answer in this third and final article in this series! We’re going to look at the trade-offs of each, including the main things that affect their performance. Then, we’ll look at how Kotlin’s sequences stack up against Java streams, a similar concept.

If you’re asking yourself what a sequence is, you should definitely read the first article in this series, Kotlin Sequences: An Illustrated Guide. And if you’re looking to understand exactly how they’re implemented, you should take a look at the second article, Inside Kotlin Sequences.

All caught up? Great - let’s go!

Sequences vs. Collections: Performance

Performance is tricky business. Yes, sequences are known for their performance benefits over standard collection operations. But there are certain characteristics that can totally blindside you if you’re not aware of them. In fact, in many cases, collections can be more efficient than sequences!

There are a few main factors that can affect performance. It’s helpful to think of these factors as weights on a scale - some factors will tend to tip the scales in favor of sequences, and some will tend to tip the scales in favor of collections.

Knowing these factors - and how they combine - can be a helpful starting point for understanding when sequences might be a better choice than collections. Keep in mind that multiple factors often will be in play at one time - one factor will tip the scales in one direction, but the next factor might tip it farther in the other direction.

Also, it’s critical to understand that unless you actually run benchmarks on your specific code with true-to-life data, you can’t know for sure. So keep that in mind as we explore these factors!

Factor #1: Operation Count

As you know, collection and sequence operations such as filter() , map() , and take() can be linked together into an operation chain, like this:

You might recall that collection operations chains create an intermediate collection in between the operations:

Sequence operation chains, on the other hand, do not have a need to create these intermediate collections.

The more operations you have in your chain, the more intermediate collections would be created if you were to use a collection chain. So if you’ve got an operation chain with many operations in it, a sequence would often be a better choice.

Keep in mind, though, that it’s not just the number of operations that matters. The kinds of operations can also have an incredible impact on performance! Let’s look at some examples.

Factor #2: Short-Circuiting Operations

As you might recall, one of the main differences between sequences and collections is the order that each operation is applied to each individual element. Here’s a diagram from the first article in this series, which compares how the individual elements are processed for a simple filter { ... }.map { ... }.take(5).toSet() chain, for both a collection and sequence.

Because this chain included take(5) , the sequence version was able to quit after executing just 18 operations, when it encountered it’s fifth element. Contrast that with the collection version above, which produced the same output but had to execute 31 operations to get there.

The take() function is what we might call a short-circuiting operation, because it can stop processing items once some condition has been met.

There are plenty of other short-circuiting operations, such as:

contains() and indexOf() - Once the element is located, there’s no need to continue iterating.

and - Once the element is located, there’s no need to continue iterating. any() , none() , and find() - Similarly, once an element that matches the predicate is found, there’s no need to iterate further.

, , and - Similarly, once an element that matches the predicate is found, there’s no need to iterate further. first() - Naturally, this one can short-circuit after it gets the very first element.

These functions will usually tip the scales in favor of sequences, especially in cases where you’ve got:

many operations,

many elements, and…

the matching element appears toward the beginning.

Factor #3: Resizable Collections

So far, we’ve seen cases where sequences can really shine - when there are lots of operations, or when they can short-circuit. But certain operations use resizable collections, and they can drastically tip the scales back toward collections. If you don’t know to look for them, they can really catch you off guard. Let’s dig in!

Collecting Results

Often, when you’re done with all of the processing steps of a sequence, you’ll want to collect the results into a list or a set. For example: