Choosing Between String, StringBuilder, and StringBuffer for Concatenating 100 Million Strings in a JD Interview

The article dissects a JD interview question about concatenating one hundred million strings, comparing String, StringBuilder, and StringBuffer, and explains how immutability leads to object explosion, how StringBuilder’s default capacity causes costly expansions, and why StringBuffer’s synchronized methods become a performance bottleneck in high‑concurrency scenarios.

Tech Freedom Circle
Tech Freedom Circle
Tech Freedom Circle
Choosing Between String, StringBuilder, and StringBuffer for Concatenating 100 Million Strings in a JD Interview

During a JD interview, a candidate was asked to discuss the differences among String, StringBuilder, and StringBuffer when concatenating 100 million strings using a for loop. The interviewer challenged the candidate with three follow‑up questions: whether a new StringBuilder is created each iteration, whether the old content must be copied each time, and the total time cost for 100 million iterations.

Round 1 – Using String

The candidate claimed that the JVM optimises String concatenation, writing the following code on the whiteboard:

String s = "";
for (int i = 0; i < 10000; i++) {
    s += i; // assume compiler optimisation
}

The interviewer pointed out three facts:

Each loop creates a new StringBuilder, copies the previous String content, appends the new value, and then creates a new String via toString() – an "object explosion".

These temporary objects trigger frequent GC, causing CPU spikes and possible STW pauses.

Assuming 10 ns per concatenation, 100 million iterations would take roughly 17 minutes.

Round 2 – Using StringBuilder

The candidate switched to:

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i);
}

The interviewer then asked about default capacity (16), how many expansions occur for 10 000 iterations, and the impact of those expansions on CPU.

Key points about StringBuilder:

Backed by a mutable char[] (see AbstractStringBuilder source). Append runs in amortised O(1).

Default capacity is 16; each expansion follows newCap = oldCap*2 + 2, invoking Arrays.copyOfSystem.arraycopy, a native memcpy that can cause noticeable CPU spikes when repeated.

For 1 billion characters, about 25 expansions are required; pre‑setting capacity (e.g., new StringBuilder(100_000)) eliminates these copies.

Calling append(null) throws NullPointerException, unlike String which converts null to the literal "null". toString() creates a new String; since JDK 7u6 it copies the buffer instead of sharing the array.

Round 3 – Using StringBuffer

The candidate finally wrote:

StringBuffer sb = new StringBuffer();
// multiple threads append ...

The interviewer highlighted that StringBuffer adds synchronized to every mutating method, turning a potentially parallel workload into a serial one. With 100 threads, 99% of threads block, reducing throughput dramatically (estimated 50× slower than StringBuilder).

Lock‑coarsening and lock‑elimination are only effective when the object does not escape the thread (i.e., no sharing). In real concurrent use, these JIT optimisations disappear.

Fundamental Analysis of String

String

is immutable: declared as public final class String with a private final char[] value. No setters exist, so any modification creates a new object. Benefits include thread‑safety, cached hash code, and string‑pool reuse. The downside is massive object creation when used in a loop, leading to O(n²) time.

JVM Optimisation Boundaries

The compiler only folds compile‑time constant concatenations (e.g., "a" + "b" + "c") into a single constant. Runtime‑dependent concatenations, especially inside loops, are compiled to the pattern shown above, with a new StringBuilder per iteration.

Best‑Practice Recommendations

For small, single‑shot concatenations, the + operator is fine.

For large or looped concatenations, use StringBuilder with an appropriate initial capacity to avoid expansions.

Never share a StringBuilder across threads; use thread‑local instances or the "split‑and‑merge" pattern.

ExecutorService executor = Executors.newFixedThreadPool(10);
List<Future<StringBuilder>> tasks = new ArrayList<>();
for (int t = 0; t < 10; t++) {
    int id = t;
    tasks.add(executor.submit(() -> {
        StringBuilder sb = new StringBuilder(100_000);
        for (int i = 0; i < 10_000; i++) {
            sb.append("Task").append(id).append("-").append(i).append(",");
        }
        return sb;
    }));
}
StringBuilder finalSb = new StringBuilder();
for (Future<StringBuilder> f : tasks) {
    finalSb.append(f.get().toString());
}
executor.shutdown();

When thread‑safety is required, prefer a ThreadLocal<StringBuilder> or the split‑and‑merge approach rather than StringBuffer, which incurs full‑method synchronization.

Remember to clear or remove ThreadLocal instances after use to avoid memory leaks.

Conclusion

The interview demonstrated three layers of failure: misunderstanding String immutability, overlooking StringBuilder expansion costs, and treating StringBuffer as a high‑performance concurrent solution. A solid answer must cover the underlying JVM behaviour, quantify GC and CPU impact, and propose scalable, lock‑free designs for massive string concatenation.

concurrencystringgcstringbuilderstringbuffer
Tech Freedom Circle
Written by

Tech Freedom Circle

Crazy Maker Circle (Tech Freedom Architecture Circle): a community of tech enthusiasts, experts, and high‑performance fans. Many top‑level masters, architects, and hobbyists have achieved tech freedom; another wave of go‑getters are hustling hard toward tech freedom.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.