skip to content

Why can converting a small list to a Sequence actually be slower, and how would you decide whether a Sequence is worth it?

level: middleimportance: should knowfreq 55%

answer

  1. Sequence = iterator wrapper + lambda per element
  2. Eager = tight indexed loop, JIT-friendly
  3. Small data: overhead > allocation savings
  4. 2+ of: big data / long chain / short-circuit -> Sequence
  5. Measure with JMH, don't guess

basics

~10 s

Sequences add overhead per element: iterator objects and lambda calls for every step. On small lists or a single operation, that overhead beats the savings, so plain List operators are faster.

solid answer

~40 s

Each `Sequence` stage wraps the previous one in an iterator and invokes its lambda per element, so a chain pays virtual-call and allocation overhead for the wrapper iterators on every element. Eager `List` operators are tight indexed/array-backed loops and can be friendlier to the JIT and to inlining. For a small dataset the allocation savings from avoiding one intermediate list are tiny, while the per-element iterator overhead is paid in full — so the eager version usually wins. The decision factors: data size (large favors Sequence), chain length (many steps favor Sequence), and short-circuiting (early-exit favors Sequence). For a single `map` or `filter` on a small list, never bother with `.asSequence()`. When unsure, **measure** with a tool like JMH rather than guessing; micro-differences are easy to get wrong.

go deeper

for a junior

Recognizes that Sequences are not always faster.

for a middle

Explains per-element iterator/lambda overhead and gives a size/chain/short-circuit checklist.

for a senior

Discusses JIT inlining, allocation profiles, and insists on measurement for hot paths.

for a principal

Sets team guidelines (default eager, Sequence on justified criteria) and budgets benchmarking effort against impact.

## The hidden cost of laziness A `Sequence` is not free. Every intermediate operator (`map`, `filter`, …) returns a new `Sequence` whose iterator wraps the upstream iterator. When a terminal op drives the chain, *each element* passes through a stack of iterator `next()`/`hasNext()` calls plus the operator's lambda. That is per-element overhead: object indirection, megamorphic call sites, and lambda dispatch. Eager `List`/`Array` operators, by contrast, are typically simple loops that read by index or via a single iterator and append to a pre-sized result. The JIT inlines and optimizes these well, and for an array-backed `List` access is cache-friendly. ## Why small data flips the trade-off The *benefit* of a Sequence is avoiding intermediate-list allocations and enabling short-circuiting. The *cost* is per-element iterator/lambda overhead. On a 5- or 50-element list: - The avoided allocation is one small short-lived array — cheap, and the GC handles it trivially. - The per-element overhead is paid for every element of every stage. So the cost dominates the benefit and the eager version is faster. ```kotlin val small = listOf(1, 2, 3, 4, 5) // Faster here: plain eager operators val eager = small.filter { it > 1 }.map { it * 2 } // Slower here: pointless Sequence wrapping for 5 elements val lazy = small.asSequence().filter { it > 1 }.map { it * 2 }.toList() ``` ## A decision checklist Favor a **Sequence** when **two or more** of these hold: - Large or unbounded dataset (thousands+ elements, or `generateSequence`). - Long chain of operators (3+ `map`/`filter` steps). - A short-circuiting terminal (`first`, `take`, `any`, `find`). Favor **eager collection ops** when: - Small collection. - One or two operators. - You need the whole result materialized anyway. ## Measure, don't guess Micro-benchmarks here are notoriously misleading because of JIT warmup and dead-code elimination. Use **JMH** (e.g., `kotlinx-benchmark`) with proper warmup and `Blackhole` if a hot path matters; otherwise prefer readability.

  • Is there extra allocation cost in the Sequence itself, beyond per-element overhead?
    Yes — each intermediate operator allocates a wrapper Sequence/iterator object, and capturing lambdas may allocate too, though many simple lambdas are optimized to singletons.
  • Would you reach for a Sequence by default for readability?
    No. Default to plain collection operators for clarity and speed on typical small data; switch to Sequence only when size, chain length, or short-circuiting justify it.

saying these in an interview costs you the question

  • Always using asSequence() as a habit
  • Claiming Sequences have zero overhead
  • Ignoring data size when choosing
  • Hand-wavy benchmarking without warmup
  • Thinking a single map is cheaper as a Sequence

context