Given that Java Strings are immutable, what are the performance implications of the String API in loops, and how do StringBuilder, the '+' operator, and intern() fit in?
answer
- immutable: every op returns a new String
- '+' in a loop = O(n^2) + GC churn → use StringBuilder
- single a+b+c is fine (invokedynamic/StringConcatFactory)
- literals are pooled; == on runtime strings is false → use equals
- intern() dedupes into the pool; niche, GC dedup often better
basics
~20 sEvery String operation creates a new String, so building one piece-by-piece with '+' in a loop is slow because it makes many throwaway strings. Use StringBuilder to build in a loop; it edits a buffer in place.
solid answer
~50 sStrings are immutable, so methods like substring, concat, replace, toUpperCase all return new String objects rather than modifying in place. A single 'a + b + c' is fine — modern compilers lower it via invokedynamic/StringConcatFactory into efficient code — but concatenating with '+' inside a loop is O(n^2) because each iteration copies the growing string into a new one. Use StringBuilder (or StringBuffer if you need thread safety) to append into a resizable buffer and call toString once. The String pool stores unique literal instances so equal literals share one object; intern() lets you place a runtime-built string into that pool to dedupe and enable == comparison, but it's rarely worth it and can pressure memory. Immutability also buys safe sharing across threads, safe use as HashMap keys (cached hashCode), and security for things like file paths.
code
java · 15 lines// Slow: O(n^2), many throwaway Strings
String bad = "";
for (String p : parts) bad += p;
// Fast: O(n), one buffer
StringBuilder sb = new StringBuilder(parts.size() * 16);
for (String p : parts) sb.append(p);
String good = sb.toString();
// Comparison: literals are pooled, runtime strings are not
String a = "hi";
String b = new String("hi");
boolean sameRef = (a == b); // false
boolean sameContent = a.equals(b); // true
boolean pooled = (a == b.intern()); // truego deeper
Knows strings are immutable and that StringBuilder should be used to build strings in a loop, and compares with equals not ==.
Explains why loop concatenation is slow, uses StringBuilder correctly (pre-sizing), and knows literals are pooled while runtime strings are distinct.
Articulates O(n^2) vs O(n), the single-expression invokedynamic optimization, StringBuilder vs StringBuffer, and what intern()/the pool do.
Weighs immutability's system-wide benefits (thread-safety, caching, security), guides on intern() vs GC string deduplication, and sets string-building/comparison standards in reviews.
## Immutability — the foundation A Java `String` is **immutable**: its contents can never change after construction. Every 'mutating-looking' method (`substring`, `concat`, `replace`, `toUpperCase`, `trim`, `strip`, …) actually returns a **new** `String` and leaves the receiver untouched. This is a deliberate design choice with big consequences for correctness and performance. ### Benefits immutability buys - **Thread safety for free:** an immutable object can be shared across threads with no synchronization, because no thread can change it. - **Safe map keys:** because the content (and thus `hashCode`) never changes, a String is a reliable `HashMap`/`HashSet` key; `hashCode` is even **cached** after first computation. - **Security:** values like file paths or connection strings can't be altered by another reference holder after a check. - **Interning/pooling:** identical immutable values can be shared safely. ## The cost: concatenation in loops Because you can't grow a String in place, building one by repeated concatenation copies the entire current content each time. With a loop body `r = r + p`, each `+` copies all prior chars. If the result has total length n, this is roughly **O(n^2)** work and allocates many short-lived intermediate strings, stressing the garbage collector. This is one of the most common Java performance mistakes. ### Why a single expression is fine A one-shot `a + b + c` is **not** the same trap. The Java compiler lowers string concatenation; on modern JDKs (9+) it uses an `invokedynamic` call to `StringConcatFactory`, which builds the result in a single efficient step. So readable `+` for a fixed number of parts is fine — the problem is specifically `+` **accumulating inside a loop**. ## StringBuilder — the right tool for loops `StringBuilder` is a **mutable** character buffer. `append` adds to it in place (amortized O(1), the backing array doubles as needed), and a final `toString()` materializes one String. The loop becomes O(n). Pre-size with `new StringBuilder(expectedLength)` to avoid regrowth. `StringBuffer` is the older, **synchronized** sibling — same API but every method is thread-safe (and slower); use it only when one buffer is genuinely shared across threads, which is rare. For streams, `Collectors.joining` does this internally. ## The String pool and intern() Java keeps a **String pool** (a table of unique String instances). **String literals** are automatically interned: two occurrences of `"abc"` in source refer to the **same** object, so `"abc" == "abc"` is true. Strings built at runtime (e.g. via concatenation of variables or `new String`) are **distinct** objects, so `==` on them is false even when contents are equal — which is exactly why you compare strings with `.equals()`, never `==`. `String.intern()` looks up the pool: if an equal string is already there it returns that canonical instance; otherwise it adds this one and returns it. This can **deduplicate** memory for many equal runtime strings and let you use `==`. But it's a niche optimization: interning has overhead, can hold strings longer than you want, and modern GCs offer automatic **string deduplication** (e.g. G1's `-XX:+UseStringDeduplication`) that often makes manual `intern()` unnecessary. Overusing `intern()` can also pressure the pool. ## Practical guidance - Accumulating in a loop → `StringBuilder` (pre-sized when you can). - A fixed, small number of parts → plain `+` or `String.format`/`join` for clarity. - Comparing content → `equals`/`equalsIgnoreCase`, never `==`. - Reach for `intern()` only with measured, massive duplication; otherwise consider GC string dedup.
- Why is r = r + p inside a loop O(n^2) but a + b + c is fine?In the loop each concatenation copies the entire current string into a new one, so total work grows quadratically. A single a + b + c is compiled (via StringConcatFactory) into one combined build, copying each part once.
- How does immutability help String be a good HashMap key?Because the content never changes, its hashCode is stable (and cached), so the key always lands in the same bucket and stays findable — a mutable key whose hash changed after insertion would be lost.
saying these in an interview costs you the question
- Using '+=' to build a string inside a loop
- Claiming a single a + b + c expression is slow
- Comparing strings with == instead of equals
- Thinking StringBuilder is thread-safe (that's StringBuffer)
- Believing intern() is a free win rather than a niche, measured optimization