Using += to build strings in Java loops creates a new String object on every iteration. Learn why this is O(n²) and how StringBuilder, StringJoiner, and streams fix it.
JOptimize Team
Using += to build strings in a Java loop is one of the oldest and most common performance mistakes in the language. It looks harmless for small inputs and becomes catastrophic at scale. The reason is fundamental to how Java strings work — and understanding it makes the fix obvious.
Java String objects are immutable. Every += operation doesn't append — it creates a brand new String object containing the old content plus the new addition, then discards the old one.
// What this looks like: String result = ""; for (String item : items) { result += item + ", "; // ↠Creates a NEW String on every iteration }
What happens at iteration N:
(previous length + item.length + 2)String wrapping the arrayWith 1000 items averaging 10 chars each, you copy roughly:
10 + 20 + 30 + ... + 10,000 = ~50,000,000 characters
That's O(n²) copying for what should be O(n) work.
// Benchmark with 10,000 strings of 10 chars each // String += : ~850ms String result = ""; for (int i = 0; i < 10_000; i++) { result += data[i]; } // StringBuilder: ~2ms StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10_000; i++) { sb.append(data[i]); } String result = sb.toString();
425x slower for 10,000 iterations. At 100,000 iterations the gap grows to thousands.
// Before public String buildCsv(List<String> values) { String result = ""; for (String value : values) { result += value + ","; // ⌠} return result.replaceAll(",$", ""); } // After public String buildCsv(List<String> values) { StringBuilder sb = new StringBuilder(); for (String value : values) { sb.append(value).append(","); // ✅ No new objects } if (sb.length() > 0) sb.setLength(sb.length() - 1); // Remove trailing comma return sb.toString(); }
Pre-size StringBuilder if you know the approximate length:
// Pre-sizing avoids internal array resizing int estimatedSize = values.stream().mapToInt(String::length).sum() + values.size(); StringBuilder sb = new StringBuilder(estimatedSize);
// Joining with delimiter — simplest approach List<String> names = List.of("Alice", "Bob", "Charlie"); String result = String.join(", ", names); // ✅ "Alice, Bob, Charlie" // With prefix and suffix StringJoiner joiner = new StringJoiner(", ", "[", "]"); for (String name : names) { joiner.add(name); } String result = joiner.toString(); // ✅ "[Alice, Bob, Charlie]"
// Collecting to string with Collectors.joining List<Order> orders = orderRepository.findAll(); // Simple join String orderIds = orders.stream() .map(o -> String.valueOf(o.getId())) .collect(Collectors.joining(", ")); // ✅ // With prefix/suffix String csv = orders.stream() .map(o -> o.getId() + "|" + o.getStatus()) .collect(Collectors.joining("\n", "id|status\n", ""));
Modern JVM (Java 9+) uses invokedynamic to optimize + concatenation:
// This is optimized by the JIT in Java 9+ String msg = "Order " + orderId + " by " + customerName + " total: " + total; // Compiled to a single StringConcatFactory call — efficient
But the JIT optimization does NOT apply inside loops:
// Loop += is NOT optimized — still O(n²) for (String item : items) { result += item; // JIT can't help here }
The rule: single-line concatenation outside loops is fine. += inside loops is always a problem.
+= in loops, even "small" ones — what's small in dev (100 items) can be large in prod (100,000 items)StringBuffer instead of StringBuilder — StringBuffer is synchronized (thread-safe) which adds overhead; use StringBuilder unless you explicitly need thread safetyPreparedStatement or JPAString.format() in tight loops — String.format() uses regex internally and is slower than StringBuilder.append() for high-frequency formattingString += in Java loops is O(n²) because each operation copies the entire string. Fix it with StringBuilder.append() for general cases, String.join() or StringJoiner for delimiter-separated values, and Collectors.joining() in streams. The JIT optimizes single-line concatenation but cannot help with loop-based +=.
JOptimize detects += string concatenation inside loops and generates the correct StringBuilder replacement with a single click.
+= in loops inline, Claude AI generates the StringBuilder fix: Install JOptimize for IntelliJFix string concatenation performance issues across your entire codebase — 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.