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.
answer
- asSequence fuses transforms → single pass, no intermediate lists
- Int sum/fold overflows silently → accumulate in Long
- minByOrNull = element, minOfOrNull = selector value
- *OrNull → null on empty; minOf/maxOf → throw
- fold once for multiple aggregates / one-shot sequences
basics
~20 sOn 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 sEager 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// 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
Aware that summing big numbers can overflow and that sequences exist.
Uses Long accumulation and knows asSequence avoids intermediate lists.
Distinguishes by/of/natural min-max variants, reasons about pass counts, and folds once for multiple aggregates.
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)