skip to content

How do the `build*` functions compare to `mutableListOf().apply { }.toList()` or `StringBuilder().apply { }.toString()`? What concrete advantage do they add?

level: middleimportance: should knowfreq 45%

answer

  1. buildString == StringBuilder().apply{}.toString()
  2. buildList returns List, no .toList() copy needed
  3. apply leaks MutableList unless you copy
  4. All inline -> no lambda allocation
  5. Use apply when you want to keep mutating

basics

~10 s

They do the same thing but in one step: create a mutable builder, let you fill it, and return a read-only result. buildList skips the extra .toList() copy and gives a cleaner, intention-revealing call.

solid answer

~40 s

Functionally `buildList { ... }` is close to `mutableListOf<E>().apply { ... }`, but with two differences. First, the **return type is the read-only interface** (`List`) instead of `MutableList`, so you don't leak mutability to callers without writing `.toList()`. Second, `buildList` builds the result **without a defensive copy** — `apply { }.toList()` allocates a second list, while `buildList` returns the same backing collection frozen behind a read-only type, so it's one allocation. For strings, `buildString { }` equals `StringBuilder().apply { }.toString()` exactly (it's literally defined that way) but is more concise. All `build*` functions are `inline`, so the lambda is not allocated. Net: less ceremony, no accidental mutable leak, and (for collections) one fewer copy than `apply { }.toX()`.

go deeper

for a junior

Recognizes both produce filled collections/strings and build* is shorter.

for a middle

Explains the read-only return type and that build* avoids the .toList() copy.

for a senior

Quantifies the allocation difference and names when apply is still the right tool.

for a principal

Reasons about API design: returning the narrowest (read-only) type, avoiding mutable leakage across boundaries, and copy elision trade-offs.

## The equivalences ```kotlin // buildString is literally defined as: fun buildString(action: StringBuilder.() -> Unit): String = StringBuilder().apply(action).toString() // buildList is conceptually: fun <E> buildList(action: MutableList<E>.() -> Unit): List<E> = ArrayList<E>().apply(action) // returned as read-only List, no copy ``` ## Three concrete advantages over the manual `apply` idiom 1. **No mutable leak.** `mutableListOf<E>().apply { ... }` returns a `MutableList`. To hand callers an immutable view you must add `.toList()` (which also copies). `buildList` already returns `List<E>`. 2. **One allocation for collections.** `apply { }.toList()` allocates the builder *and* a defensive copy. `buildList` returns the builder itself, typed as read-only — no second array. (You give up the ability to keep mutating it, which is exactly the point.) 3. **Conciseness + intent.** `build*` reads as "construct this collection," which is clearer than `apply`'s general-purpose "configure receiver, return receiver." ## When `apply` is still right - You genuinely want to keep a **mutable** result and keep editing it later. - You're configuring an object that is **not** a collection/StringBuilder (e.g., a request builder, a `mutableMapOf` you'll pass around mutably). - You need the receiver's own return value semantics — remember `apply` returns the receiver; `let`/`run`/`with` differ. ## Cost comparison table | Idiom | Returns | Extra copy? | |---|---|---| | `buildList { }` | `List` (read-only) | No | | `mutableListOf().apply { }` | `MutableList` | No, but leaks mutability | | `mutableListOf().apply { }.toList()` | `List` | Yes (defensive copy) | ## Inline note All `build*` functions are `inline`, and so is `apply`. So neither approach allocates a lambda object; the difference is purely about the defensive copy and the returned type.

  • Does `buildList` make a defensive copy of the built list before returning?
    No — it returns the same backing list typed as read-only `List`, saving the copy that `apply { }.toList()` would make.
  • Why not just always use `apply`?
    `apply` returns the mutable receiver, so you either leak mutability or pay for a copy; `build*` returns a read-only type directly and reads as construction.

apply hands you back the open editing tool; build* hands you back the sealed finished product.

saying these in an interview costs you the question

  • Claiming `buildList` does an extra defensive copy like `.toList()`
  • Saying there's no functional difference at all from `apply`
  • Forgetting that `apply` returns the mutable receiver
  • Thinking the lambda is heap-allocated (both are inline)

context