Sobes.tech
Middle

How to work with streams in Kotlin?

sobes.tech AI

Answer from AI

In Kotlin, for working with data streams (similar to Java 8 Streams API), extension functions for collections provided by the standard library are used. This is not a separate API, but a set of methods within kotlin.collections.

The main operations on streams are divided into:

  • Intermediate operations: Return a new collection and are "lazy" (executed only when a terminal operation is called). Examples: filter, map, flatMap, distinct, sorted, take, drop.
  • Terminal operations: End the sequence of operations and return a result (not a collection). They trigger the execution of all intermediate operations. Examples: forEach, reduce, fold, count, sum, average, min, max, toList, toSet, toMap.

There is also a concept of sequences, which provides explicit laziness. Sequences process elements one by one as needed, which can be more efficient for large collections or long chains of operations:

  • Creating a sequence: collection.asSequence()
  • Converting back to a collection: sequence.toList()
// Example of using standard functions for stream processing
fun processCollection() {
    val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

    val evenSquares = numbers
        .filter { it % 2 == 0 } // Intermediate operation: filter even numbers
        .map { it * it }     // Intermediate operation: square

    println(evenSquares) // Terminal operation: print collection (performs filter and map)

    val sumOfOdd = numbers
        .filter { it % 2 != 0 } // Intermediate
        .sum()                // Terminal (computes sum of odd numbers)

    println(sumOfOdd)
}

// Example of using sequences
fun processSequence() {
    val largeCollection = (1..1_000_000).toList()

    // With collections (not lazy): filter and map are executed for all elements immediately
    val resultCollection = largeCollection
        .filter { it % 2 == 0 }
        .map { it * 2 }
        .take(10) // take(10) is executed after filtering and mapping for all elements

    println("Result Collection size: ${resultCollection.size}")

    // With sequences (lazy): filter, map, and take are executed element by element
    val resultSequence = largeCollection.asSequence()
        .filter { it % 2 == 0 } // filter executed for each element in turn
        .map { it * 2 }     // map executed for each filtered element
        .take(10)         // take(10) stops processing after 10 elements

    println("Result Sequence size: ${resultSequence.toList().size}") // toList() triggers sequence execution
}

fun main() {
    processCollection()
    processSequence()
}

Comparison table of standard functions and sequences regarding laziness:

Feature Standard collection functions Sequences
Laziness Implicit laziness for intermediate operations, but terminal operation processes the entire chain Explicit laziness: operations are performed element by element as needed
Efficiency May be less efficient for large collections and long chains due to intermediate collections More efficient for large collections and long chains
Application Suitable for small and medium collections Suitable for large collections or operations that can be early terminated (take, find, first, any, all, none)

Using standard functions on collections is often sufficient and more concise. Sequences are recommended when explicit laziness is needed to optimize performance when working with large data volumes.