skip to content

Why is building a long string by concatenating with += inside a loop inefficient, and what does StringBuilder do differently?

level: juniorimportance: must knowfreq 70%

answer

  1. String immutable -> += copies everything
  2. Loop concat is O(N^2), N garbage objects
  3. StringBuilder = one mutable buffer, amortized O(N)
  4. toString() once at the end
  5. buildString { } is the Kotlin idiom (inline)

basics

~10 s

Kotlin Strings are immutable, so each += makes a brand-new String and copies everything. In a loop that repeats over and over. StringBuilder keeps one growing buffer and just appends, avoiding all those copies.

solid answer

~40 s

In Kotlin a `String` is immutable, so `s += part` does not mutate `s` — it allocates a new `String` and copies the old contents plus the new piece. Inside a loop of N iterations this is roughly O(N²) work and produces N throwaway objects, hammering the garbage collector. `StringBuilder` is a mutable character buffer: `append` writes into an internal array, only resizing (doubling) occasionally, so total work is amortized O(N). You build incrementally and call `toString()` once at the end. Kotlin's idiomatic wrapper is the `buildString { }` inline function, which creates a `StringBuilder`, runs your lambda with it as the receiver, and returns `toString()` for you. Use it for any non-trivial loop concatenation.

code

kotlin · 8 lines
kotlin
// Slow: quadratic, lots of garbage
var bad = ""
for (i in 1..1000) bad += i

// Fast: amortized linear
val good = buildString {
    for (i in 1..1000) append(i)
}

go deeper

for a junior

States String is immutable and that StringBuilder avoids repeated copying; can write a basic append loop.

for a middle

Quantifies O(N^2) vs amortized O(N), mentions GC pressure, and reaches for buildString as the idiom.

for a senior

Explains capacity doubling/amortization precisely and notes the compiler already fuses single-expression concatenation, so only loops matter.

for a principal

Frames it as a general mutable-accumulator pattern, discusses pre-sizing capacity and when micro-optimizing concatenation is premature vs. justified by profiling.

## The problem: String immutability In Kotlin, `String` is **immutable** — once created, its characters never change. The `+` / `+=` operators on strings do not modify anything in place; they create a **new** `String` whose contents are the concatenation. ```kotlin var s = "" for (i in 1..n) { s += i // allocates a NEW String each time, copying all previous chars } ``` Each `+=` copies the entire current contents into a fresh buffer. If the result grows to length N, iteration `k` copies ~k characters, so the total copying is `1 + 2 + ... + N ≈ N²/2` — **quadratic (O(N²))** time, plus N short-lived String objects for the garbage collector. ## The fix: StringBuilder `StringBuilder` is a **mutable** sequence of characters backed by a resizable array. `append` writes into that array; when the array fills, it grows (typically doubling capacity). Because doubling happens rarely, the *amortized* cost of N appends is **linear (O(N))**. ```kotlin val sb = StringBuilder() for (i in 1..n) { sb.append(i) } val result: String = sb.toString() // materialize once at the end ``` You mutate one object the whole time and convert to an immutable `String` exactly once via `toString()`. ## The idiomatic Kotlin form: buildString `buildString` is an **inline** stdlib function. It creates a `StringBuilder`, invokes your lambda with that builder as the **receiver** (so `this` is the builder and you call `append` directly), then returns `toString()`: ```kotlin val result = buildString { for (i in 1..n) append(i) // 'this' is the StringBuilder } ``` This is the preferred Kotlin idiom: it scopes the builder, removes the manual `toString()`, and because it is `inline` there is no lambda allocation overhead. ## Key terms - **Immutable**: state cannot change after construction. - **Amortized cost**: average cost per operation across many operations, smoothing out the occasional expensive resize. - **Receiver lambda**: a lambda whose `this` is set to a given object, letting you call its members without a qualifier.

  • Is `"a" + "b" + "c"` on a single line also a problem?
    No. The compiler optimizes a fixed-length expression of constants/few operands (often into a single StringBuilder or a constant). The pathological case is repeated reassignment inside a loop.
  • Does StringBuilder make the final String mutable?
    No. `toString()` produces a normal immutable String; the builder's mutability only exists during construction.

+= in a loop is like recopying an entire shopping list by hand each time you add one item; StringBuilder is just writing the next line on the same sheet.

saying these in an interview costs you the question

  • Claiming Kotlin Strings are mutable
  • Saying += in a loop is fine / O(N)
  • Not knowing buildString exists
  • Thinking StringBuilder returns a mutable string from toString()
  • Calling toString() inside the loop on every iteration

context