How do the `build*` functions compare to `mutableListOf().apply { }.toList()` or `StringBuilder().apply { }.toString()`? What concrete advantage do they add?
answer
- buildString == StringBuilder().apply{}.toString()
- buildList returns List, no .toList() copy needed
- apply leaks MutableList unless you copy
- All inline -> no lambda allocation
- Use apply when you want to keep mutating
basics
~10 sThey 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 sFunctionally `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
Recognizes both produce filled collections/strings and build* is shorter.
Explains the read-only return type and that build* avoids the .toList() copy.
Quantifies the allocation difference and names when apply is still the right tool.
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)