Collection Transformations Eager Intermediate Allocation

Explaining Chaining and Allocations

When chaining map and filter, each function creates a new collection even if there are no intermediate variables shown in the code.

Example (chained)

val evensSquared = listOf(1,2,3,4)
    .filter { it % 2 == 0 }     // creates List<Int>
    .map    { it * it }         // creates List<Int>

Here, filter creates one list, followed by map creating another. Each step results in new allocations in memory, even if we don't see intermediate variables.

Concept: Eager, Chained Transformations Allocate at Each Step

Key Idea

Even when you write transformations in a single chain, each call still allocates its own new collection under the hood.


Chained Example

val numbers = listOf(1, 2, 3, 4)

// Single expression, but two allocations occur:
val evensSquared: List<Int> = numbers
  .filter { it % 2 == 0 }   // ① creates a new List<Int> containing [2, 4]
  .map    { it * it }       // ② creates another new List<Int> containing [4, 16]

// numbers        == [1, 2, 3, 4]
// result of ①   == [2, 4]
// result of ②   == [4, 16]

Why It Matters

  • Memory: Each step briefly holds its own list.
  • Performance: Two full passes over the data.

Quick Optimizations

  • Combine in one pass when logic allows:

    val evensSquared = numbers.mapNotNull { 
      if (it % 2 == 0) it * it else null 
    }
    
  • Reuse the result

    If you need the same result consider re-using the result.

  • Lazy pipeline with Sequence to defer allocations until the end:

    val evensSquared = numbers
      .asSequence()
      .filter { it % 2 == 0 }
      .map    { it * it }
      .toList()  // one allocation only