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