skip to content

What is the capacity argument to buildList/buildSet/buildMap, and when does using a builder improve performance over a chain of operators?

level: middleimportance: should knowfreq 28%

answer

  1. Optional capacity = pre-size backing collection
  2. Negative capacity throws IllegalArgumentException
  3. One allocation/one pass vs chained operators' many intermediates
  4. Great for conditional inserts
  5. Sequence is the lazy alternative

basics

~20 s

Each builder accepts an optional initial capacity so the backing collection is pre-sized and resizes less. Builders also let you assemble a result in one pass with conditional logic, avoiding the many intermediate lists that chained operators create.

solid answer

~40 s

All three builders have an overload taking `capacity: Int` as the first argument: `buildList(capacity) { }`, `buildSet(capacity) { }`, `buildMap(capacity) { }`. It pre-sizes the backing `ArrayList`/`HashMap` so it doesn't repeatedly grow and recopy as you add elements — useful when you know the approximate final size. Passing a negative capacity throws `IllegalArgumentException`. Performance-wise, builders shine when you'd otherwise chain `map`/`filter`/`plus` operators that each allocate a new intermediate `List`; a single `buildList` block does it in one allocation with plain `add` calls and `if` branches. For large pipelines, `Sequence` is the lazy alternative, but for eager 'build a collection with some conditional inserts' logic, `buildList` is both clearer and cheaper than `listOfNotNull` + concatenation or repeated `+`.

code

kotlin · 4 lines
kotlin
val ids = buildList(capacity = items.size) {
    for (i in items) if (i.active) add(i.id)
    addAll(extraIds)
} // single allocation vs filter().map().plus()

go deeper

for a junior

Knows you can pass an initial capacity and that builders avoid manual list-then-copy.

for a middle

Explains pre-sizing to avoid regrowth, the negative-capacity exception, and one-allocation builds vs multi-allocation operator chains.

for a senior

Compares builder vs Sequence for eager vs lazy pipelines and judges when the allocation savings actually matter.

for a principal

Weighs readability vs micro-optimization, sets team conventions for builder vs operator-chain usage, and reasons about capacity hints in hot paths.

## The capacity overload Each builder has two forms: ```kotlin public inline fun <E> buildList(builderAction: MutableList<E>.() -> Unit): List<E> public inline fun <E> buildList(capacity: Int, builderAction: MutableList<E>.() -> Unit): List<E> ``` `capacity` is the **initial capacity** of the backing collection (an `ArrayList` for list/set-ish backing, a `HashMap`/`LinkedHashMap` for maps). Pre-sizing avoids the grow-and-recopy cycle that happens when an ArrayList exceeds its current array and allocates a larger one. A **negative** capacity throws `IllegalArgumentException`. ```kotlin val rows = buildList(capacity = 10_000) { repeat(10_000) { add(it) } // no intermediate array regrowth } ``` Capacity is a hint about sizing, not a hard limit — you can add more than `capacity` elements; it just resizes after that. ## When a builder beats operator chains Chained collection operators are **eager** and each returns a new list: ```kotlin val r = items .filter { it.active } // new List .map { it.id } // another new List .plus(extraIds) // another new List ``` That's three allocations. Equivalent builder: ```kotlin val r = buildList(items.size) { for (it in items) if (it.active) add(it.id) addAll(extraIds) } ``` One allocation, one pass, and conditional logic reads naturally. Builders are especially good when: - The result is assembled from **heterogeneous conditional inserts** (some 'if' add this, sometimes add that). - You'd otherwise reach for `listOfNotNull(...)` plus concatenation. - You want to **pre-size** a large collection. ## Builders vs Sequence `Sequence` is the lazy alternative that also avoids intermediate lists, processing element-by-element. Use a `Sequence` for long lazy transformation pipelines; use `buildList` for eager imperative assembly where you know you want the whole collection materialized. ## Don't over-optimize For small collections the allocation difference is negligible; prefer whichever is clearer. The capacity hint matters mainly for large, known-size builds.

  • What happens if you pass a negative capacity?
    It throws IllegalArgumentException at runtime.
  • Does capacity cap how many elements you can add?
    No, it's only an initial sizing hint; you can exceed it and the collection resizes.

saying these in an interview costs you the question

  • Thinking capacity is a maximum size limit
  • Claiming chained operators don't allocate intermediates
  • Not knowing the capacity overload exists
  • Believing builders are always faster even for tiny collections

context