Back to Blog
javaperformancestringstringbuilderjava-21optimization

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

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.

J

JOptimize Team

May 23, 2026· 6 min read

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.


Why String += in a Loop Is O(n²)

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:

  1. Allocate a new char array of size (previous length + item.length + 2)
  2. Copy all characters from the previous string into it
  3. Append the new characters
  4. Create a new String wrapping the array
  5. Discard the previous string (GC pressure)

With 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: += vs StringBuilder

// 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.


Fix 1: StringBuilder (The Classic Fix)

// 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);

Fix 2: String.join / StringJoiner (Cleaner for Delimiters)

// 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]"

Fix 3: Stream Collectors (Modern Java)

// 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", ""));

When the JIT Helps You (and When It Doesn't)

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.


Common Mistakes to Avoid

  • += in loops, even "small" ones — what's small in dev (100 items) can be large in prod (100,000 items)
  • Using StringBuffer instead of StringBuilder — StringBuffer is synchronized (thread-safe) which adds overhead; use StringBuilder unless you explicitly need thread safety
  • Building SQL with string concatenation — beyond performance, this is a SQL injection vulnerability; always use PreparedStatement or JPA
  • String.format() in tight loops — String.format() uses regex internally and is slower than StringBuilder.append() for high-frequency formatting

Summary

String += 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 +=.


Detect String Concatenation Anti-Patterns Automatically

JOptimize detects += string concatenation inside loops and generates the correct StringBuilder replacement with a single click.

Fix string concatenation performance issues across your entire codebase — 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.