Back to Blog
javaperformancestringstringbuilderjvmbest-practices

String Concatenation in Java Loops: Why += Destroys Performance (2026)

Using += to build strings inside a loop is one of the most common Java performance mistakes. Learn why it creates O(n^2) allocations and how to fix it with StringBuilder or String.join.

J

JOptimize Team

May 22, 2026· 6 min read

Using += to concatenate strings inside a loop is one of the oldest Java performance anti-patterns — and one of the most persistent. It looks harmless for 5 iterations. At 10,000 iterations, it allocates hundreds of megabytes of short-lived objects and triggers GC storms.


Why String += in Loops Is O(n^2)

Strings in Java are immutable. Every += creates a new String object by copying all previous characters plus the new ones.

// What you wrote String result = ""; for (String item : items) { result += item + ", "; // Creates a NEW String on every iteration }

What the JVM actually does:

Iteration 1: copy 0 chars + new chars   → new String (N chars)
Iteration 2: copy N chars + new chars   → new String (2N chars)
Iteration 3: copy 2N chars + new chars  → new String (3N chars)
...
Iteration k: copy (k-1)*N chars         → new String (k*N chars)

Total bytes allocated: N + 2N + 3N + ... + k*N = N * k*(k+1)/2 = O(k^2). For 10,000 strings of 10 chars each, that's ~500MB of allocations for what should be a ~100KB string.


The Fix: StringBuilder

StringBuilder maintains a mutable internal buffer and only copies when the buffer needs to grow (amortized O(1) per append):

// O(n) — correct approach StringBuilder sb = new StringBuilder(); for (String item : items) { sb.append(item).append(", "); } String result = sb.toString(); // With initial capacity estimate (avoids buffer resizing) StringBuilder sb = new StringBuilder(items.size() * 20); // estimate avg length for (String item : items) { sb.append(item).append(", "); }

Performance comparison (10,000 strings):

ApproachTimeAllocations
String += in loop~850ms~500MB
StringBuilder~2ms~200KB
String.join()~1ms~200KB

Better Alternatives for Common Patterns

Joining with a delimiter — use String.join() or StringJoiner

// String.join — cleanest for simple cases String result = String.join(", ", items); // StringJoiner — when you need prefix/suffix StringJoiner sj = new StringJoiner(", ", "[", "]"); for (String item : items) { sj.add(item); } String result = sj.toString(); // [item1, item2, item3]

Building from a stream — use Collectors.joining()

// Stream-based — readable and efficient String csv = items.stream() .filter(item -> !item.isEmpty()) .map(String::trim) .collect(Collectors.joining(", ")); // With prefix and suffix String result = items.stream() .collect(Collectors.joining(", ", "Result: [", "]"));

Building structured output — StringBuilder with capacity

public String buildReport(List<Order> orders) { StringBuilder sb = new StringBuilder(orders.size() * 50); sb.append("Order Report\n"); sb.append("============\n"); for (Order order : orders) { sb.append(order.getId()) .append(" | ") .append(order.getStatus()) .append(" | ") .append(order.getTotal()) .append("\n"); } return sb.toString(); }

What About Java's Auto-Optimization?

Modern javac compiles simple String + concatenations (outside loops) to StringBuilder or invokedynamic (Java 9+ StringConcatFactory). But inside loops, the optimizer doesn't help:

// Outside a loop — javac optimizes this to a single concat String s = "Hello" + " " + name + "!"; // Inside a loop — javac creates a new StringBuilder each iteration // (same as manual +=) for (String item : items) { result = result + item; // NOT optimized away }

Don't rely on the compiler to fix loop concatenation. Use StringBuilder explicitly.


Common Mistakes to Avoid

  • StringBuilder in a multi-threaded contextStringBuilder is not thread-safe; use StringBuffer or local variables (preferred) if sharing across threads
  • Not setting initial capacity — without it, StringBuilder starts at 16 chars and doubles on overflow; for large strings, set capacity to avoid repeated array copies
  • String.format() in a loopString.format() uses regex internally and is 5–10x slower than StringBuilder.append(); avoid it in hot paths
  • Converting StringBuilder to String inside the loop — calling .toString() inside the loop defeats the purpose; call it once after the loop

Summary

String += in loops creates O(n^2) allocations due to Java's immutable String model. Use StringBuilder for manual loops, String.join() for simple delimiter joining, and Collectors.joining() for stream-based collection. The fix is mechanical and the performance gain is dramatic — often 100x or more for large iterations.


Detect String Concatenation Issues Automatically

JOptimize scans your codebase for += String concatenation inside loops and flags every occurrence with a suggested StringBuilder or Collectors.joining() replacement. The IntelliJ plugin generates the fix with a single click.

Eliminate O(n^2) string allocations before they show up in your GC logs — free scan, no configuration required.

Want to go deeper?

Master Spring Boot, security, and Java performance with hands-on courses.

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.