skip to content

You aggregate a very large data source in Kotlin: discuss eager-vs-lazy aggregation cost, integer overflow in sum/fold, and the difference between minByOrNull and minOfOrNull.

level: seniorimportance: should knowfreq 40%

answer

  1. asSequence fuses transforms → single pass, no intermediate lists
  2. Int sum/fold overflows silently → accumulate in Long
  3. minByOrNull = element, minOfOrNull = selector value
  4. *OrNull → null on empty; minOf/maxOf → throw
  5. fold once for multiple aggregates / one-shot sequences

basics

~20 s

On big data, chaining maps before aggregating with a List creates extra lists; using asSequence avoids that. Summing many Ints can silently overflow, so sum into Long. minByOrNull returns the element with the smallest key; minOfOrNull returns the smallest key value itself.

solid answer

~40 s

Eager operators on Iterable materialize a new List per step, so list.map { ... }.filter { ... }.sum() allocates intermediates; switching to asSequence() makes the pipeline lazy and single-pass, fusing the operations and feeding the terminal aggregate (sum/fold/count) element-by-element. Aggregation terminals are themselves a single pass either way. Watch overflow: Int sum/fold wraps silently in Kotlin (no checked arithmetic), so total a large Int collection with sumOf { it.toLong() } or fold(0L). For extrema, distinguish minByOrNull/maxByOrNull (return the element minimizing/maximizing a selector) from minOfOrNull/maxOfOrNull (return the selector value) and from minOrNull/maxOrNull (natural order on the element itself). All *OrNull variants return null on empty; the non-OrNull min/max forms throw on empty. Prefer one aggregating pass over multiple traversals (e.g. fold to compute sum and count together).

code

kotlin · 15 lines
kotlin
// Overflow trap
val big = List(1_000_000) { Int.MAX_VALUE / 1000 }
val wrong: Int = big.sum()              // may overflow silently!
val right: Long = big.sumOf { it.toLong() } // safe

// Lazy single pass over a huge source
val total = generateSequence(1) { it + 1 }
    .take(1_000_000)
    .map { it * 2L }
    .sum()

// by vs of
val ps = listOf("aa", "b", "ccc")
ps.maxByOrNull { it.length } // "ccc" (element)
ps.maxOfOrNull { it.length } // 3      (value)

go deeper

for a junior

Aware that summing big numbers can overflow and that sequences exist.

for a middle

Uses Long accumulation and knows asSequence avoids intermediate lists.

for a senior

Distinguishes by/of/natural min-max variants, reasons about pass counts, and folds once for multiple aggregates.

for a principal

Weighs sequence overhead vs allocation savings, designs overflow-safe aggregation with explicit numeric types, and accounts for single-consumption semantics of streamed sources.

## Eager vs lazy aggregation Kotlin collection operators on `Iterable` are **eager**: each intermediate operator returns a brand-new `List`. ```kotlin largeList.map { it.price } // allocates List #1 .filter { it > 0 } // allocates List #2 .sum() // walks List #2 ``` That's multiple allocations and multiple passes. Converting with `asSequence()` makes the chain **lazy**: operations are *fused* and each element flows through the whole pipeline once, with no intermediate lists, terminating at the aggregate: ```kotlin largeList.asSequence() .map { it.price } .filter { it > 0 } .sum() // terminal: single pass, no intermediate List ``` For a single aggregate with **no** intermediate transforms, plain `sum()`/`fold()` is already one pass — sequences help mainly when you chain transforms before the terminal. ## Integer overflow Kotlin arithmetic is **unchecked / wrapping** by default — `Int.MAX_VALUE + 1` becomes `Int.MIN_VALUE` silently. So `sum()` / `sumOf { it.intField }` / `fold(0) { a, b -> a + b }` over many large `Int`s can **overflow without error**. Mitigations: - `sumOf { it.toLong() }` — accumulate in `Long`. - `fold(0L) { a, b -> a + b }`. - `BigInteger` for unbounded values. - `Math.addExact` / `Math.multiplyExact` if you *want* an exception on overflow. The same applies to `average()` of huge values (intermediate sum can overflow before the division). ## min/max selector variants (frequently confused) | Function | Returns | On empty | |----------|---------|----------| | `minOrNull()` / `maxOrNull()` | the smallest/largest **element** by natural order | `null` | | `minByOrNull { sel }` / `maxByOrNull { sel }` | the **element** whose selector is smallest/largest | `null` | | `minOfOrNull { sel }` / `maxOfOrNull { sel }` | the smallest/largest **selector value** | `null` | | `minOf { sel }` / `maxOf { sel }` | the selector value | **throws** `NoSuchElementException` | ```kotlin data class P(val name: String, val age: Int) val ps = listOf(P("A", 30), P("B", 20)) ps.minByOrNull { it.age } // P("B", 20) — the element ps.minOfOrNull { it.age } // 20 — the value ``` ## Single-pass aggregation If you need several aggregates, fold once instead of traversing repeatedly: ```kotlin val (sum, count) = nums.fold(0L to 0) { (s, c), x -> (s + x) to (c + 1) } ``` This avoids `nums.sum()` + `nums.count()` doing two passes (cheap for lists, but important for expensive or single-use sequences which can only be consumed once).

  • Does converting to asSequence() always speed up an aggregation?
    No. For a single terminal with no chained transforms it adds overhead. It pays off when you chain several transforms before the aggregate, or when the source is huge/streamed, because it avoids intermediate lists and short-circuits.
  • How would you detect rather than silently wrap an overflow while folding Ints?
    Accumulate via Math.addExact in the fold lambda, which throws ArithmeticException on overflow, or accumulate in Long/BigInteger and validate the range.

Eager chaining is photocopying the whole stack at every step; a Sequence is an assembly line where each item passes through all stations once.

saying these in an interview costs you the question

  • Believing Kotlin Int arithmetic is overflow-checked
  • Confusing minByOrNull (element) with minOfOrNull (value)
  • Claiming asSequence always improves aggregation performance
  • Calling minOf/maxOf on a possibly-empty source without handling the exception
  • Doing sum() then count() as separate passes on a single-use Sequence (it's already consumed)

context