Skip to Content
Course content

86: Sequences vs Collections Performance

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're running a sandwich shop. You have a huge order for 100 sandwiches. You've got three stations: one person toasts the bread, the next adds the fillings, and the last wraps them in paper.

If you work like a standard Kotlin Collection, you're doing things "eagerly." The first person toasts all 100 slices of bread and piles them onto a massive tray. Then, the second person takes that whole tray and adds fillings to all 100, piling them onto another massive tray. Finally, the third person wraps all 100. You need a lot of counter space (memory) to hold those intermediate trays, and if the customer suddenly says, "Actually, I only wanted the first sandwich," you've wasted a ton of effort preparing 99 others.

Now, imagine working as a Sequence. This is "lazy" evaluation. The first person toasts one slice and hands it immediately to the second person. They add the filling and hand it to the third person, who wraps it. One finished sandwich is delivered. If the customer says, "I only want one," you stop right there. You never toasted the other 99 slices, and you never needed those giant intermediate trays.

The Hidden Cost of Intermediate Lists

In Kotlin, whenever you call .filter or .map on a List, Kotlin creates a brand new list to hold the results of that specific step. If you have a chain of five operations on a list of 10,000 items, you might accidentally create five different lists in memory before you even get your final result. I've seen this cause surprising memory spikes in Android apps when developers process large datasets from a database.

val result = bigList
    .filter { it.isActive }   // Creates a new list
    .map { it.toDto() }       // Creates another new list
    .take(10)                 // Finally, we only wanted 10!

In the example above, we've processed the entire list twice just to keep 10 items. That's a lot of wasted CPU cycles and garbage collection pressure.

Switching to a Lazy Pipeline

To stop this, we use asSequence(). This tells Kotlin: "Don't execute these steps immediately. Just remember the plan, and only process the items when I actually ask for the final result."

val result = bigList
    .asSequence()             // Start the lazy pipeline
    .filter { it.isActive }   // No list created here
    .map { it.toDto() }       // No list created here
    .take(10)                 // Still no list!
    .toList()                 // Terminal operation: Now we actually execute

The .toList() (or .first(), .count(), etc.) is what we call a terminal operation. Nothing happens until you hit a terminal operation. Once you do, the items flow through the pipeline one by one. The first item that passes the filter is immediately mapped and sent to the take(10) bucket. Once that bucket hits 10, the whole process stops. The remaining thousands of items are never even looked at.

When Eager is Actually Faster

You might be wondering, "Why wouldn't I just use sequences for everything?" Well, there's a trade-off. Setting up the sequence machinery—the state tracking and the function wrapping—has a small overhead.

If you're dealing with a tiny list (say, 20 items) and only one or two operations, the overhead of creating a Sequence object is actually more expensive than just creating one intermediate list. In my experience, if your data set is small, stick with Collections. If you're doing complex chaining or working with large datasets, asSequence() is your best friend.




📋 Practical Task

Optimizing a High-Volume Log Filter

You are building a log analyzer that processes a list of 100,000 LogEntry objects. The goal is to find the first 5 "ERROR" level logs that contain the word "Timeout" in their message, and then extract just the timestamp of those logs.

Currently, the code is written using standard eager Collections, causing a noticeable lag and high memory usage. Your task is to refactor the following function to use a Sequence to ensure that the program stops processing as soon as the 5th match is found, avoiding the creation of unnecessary intermediate lists.

data class LogEntry(val level: String, val message: String, val timestamp: Long)

fun getFirstFiveTimeoutErrors(logs: List<LogEntry>): List<Long> {
    return logs
        .filter { it.level == "ERROR" }
        .filter { it.message.contains("Timeout") }
        .map { it.timestamp }
        .take(5)
}

Requirement: Modify the function to implement lazy evaluation and return the result as a List<Long>.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.