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.
JOptimize Team
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.
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.
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):
| Approach | Time | Allocations |
|---|---|---|
String += in loop | ~850ms | ~500MB |
StringBuilder | ~2ms | ~200KB |
String.join() | ~1ms | ~200KB |
// 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]
// 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: [", "]"));
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(); }
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.
StringBuilder in a multi-threaded context — StringBuilder is not thread-safe; use StringBuffer or local variables (preferred) if sharing across threadsStringBuilder starts at 16 chars and doubles on overflow; for large strings, set capacity to avoid repeated array copiesString.format() in a loop — String.format() uses regex internally and is 5–10x slower than StringBuilder.append(); avoid it in hot pathsStringBuilder to String inside the loop — calling .toString() inside the loop defeats the purpose; call it once after the loopString += 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.
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.
+= in loops inline and auto-generates the StringBuilder fix: Install JOptimize for IntelliJEliminate O(n^2) string allocations before they show up in your GC logs — free scan, no configuration required.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.