Java StringBuilder: Efficient String Building
java stringbuilder: Understand how Java StringBuilder works, when it beats string concatenation, and how to avoid common performance pitfalls when building strings.
When you need to assemble a string from multiple parts in Java, the + operator is the most obvious choice. For a few short concatenations, it works fine. But when the number of parts grows dynamically—inside a loop, for example—the cost of repeated concatenation becomes visible. java stringbuilder exists to handle exactly that case: a mutable, growable character sequence that lets you assemble a string without creating a new object on every operation.
Why String Concatenation Gets Expensive
Strings in Java are immutable. Every time you write str += part, the JVM allocates a new String object, copies the previous content, then appends the new part. The old object becomes garbage. For a small number of operations this is negligible. In a loop that runs thousands of times, the repeated allocation and copying adds up.
The compiler does optimize simple concatenations. When you write String s = "a" + "b" + "c";, the compiler generates a single StringBuilder internally and calls append three times. But that optimization only applies when the concatenation is a single expression. When concatenation happens across loop iterations, the compiler cannot merge the operations, and each iteration allocates a new intermediate string.
String result = ""; for (int i = 0; i < 1000; i++) { result += "item" + i + ","; // new String allocated each iteration }
Using StringBuilder avoids that repeated allocation:
StringBuilder sb = new StringBuilder(); for (int i = 0; i < 1000; i++) { sb.append("item").append(i).append(','); } String result = sb.toString();
Creating a StringBuilder
The basic usage is straightforward:
StringBuilder sb = new StringBuilder(); sb.append("Order #"); sb.append(1024); sb.append(": "); sb.append("pending"); String result = sb.toString();
The append method is overloaded for all primitive types, String, char[], and CharSequence. It returns the same StringBuilder instance, which allows chaining:
String result = new StringBuilder() .append("Order #") .append(1024) .append(": ") .append("pending") .toString();
The constructor also accepts an initial string:
StringBuilder sb = new StringBuilder("Order #");
Common StringBuilder Methods
Beyond append, StringBuilder provides several methods for modifying the sequence in place:
insert(int offset, ...)inserts content at a position, shifting existing characters to the right.delete(int start, int end)removes a range of characters.replace(int start, int end, String str)replaces a range with new content.reverse()reverses the entire sequence.setCharAt(int index, char ch)changes a single character.substring(int start, int end)extracts a portion without modifying the buffer.length()andcapacity()report the current size and the allocated array size.
A typical example is building a CSV-like line:
StringBuilder sb = new StringBuilder(); sb.append("name").append(','); sb.append("quantity").append(','); sb.append("price").append('\n'); sb.append("widget").append(',').append(3).append(',').append(9.99); String csvLine = sb.toString();
StringBuilder vs StringBuffer
Java also provides StringBuffer, which exposes the same API but synchronizes all mutator methods. That synchronization is unnecessary when the buffer is used only within a single thread, which is the common case. StringBuilder drops the synchronization and is therefore faster in single-threaded code. StringBuffer remains available for legacy compatibility, but new code should prefer StringBuilder unless the buffer is genuinely shared across threads.
| Aspect | StringBuilder | StringBuffer |
|---|---|---|
| Thread safety | Not synchronized | Synchronized |
| Single-threaded cost | Lower | Higher due to locking |
| Introduced | Java 5 | Java 1.0 |
| Recommended for | New single-threaded code | Legacy or shared access |
Performance and Capacity
StringBuilder maintains an internal char[] array. When the array is full, it allocates a larger array and copies the existing content. The default capacity is 16 characters. If you know the approximate final size, you can pass it to the constructor:
StringBuilder sb = new StringBuilder(1024);
Pre-sizing avoids repeated resizing when the final string is large. Resizing is amortized O(1) per append, but each resize copies the existing content, so pre-sizing reduces garbage and copying when the size is predictable. For a buffer that grows to tens of thousands of characters, the difference is measurable.
Common Mistakes
A frequent mistake is using StringBuilder where the compiler already handles the optimization. For a fixed number of concatenations in a single expression, + is fine and more readable. StringBuilder adds noise without benefit there.
Another mistake is calling toString() inside a loop and using the result for further concatenation. That defeats the purpose by creating new strings anyway.
Also, be careful with insert at the front of a large buffer. Inserting at index 0 shifts every existing character, making the operation O(n). If you frequently prepend, consider building the string in reverse and calling reverse() at the end.
When Not to Use StringBuilder
For simple, fixed concatenations, the + operator is clearer and the compiler handles it efficiently:
String message = "User " + userId + " has " + count + " items";
There is no reason to wrap this in a StringBuilder. The compiler generates equivalent bytecode. Use StringBuilder when the concatenation is dynamic, repeated, or spread across a loop or multiple statements.
Building Queries Conditionally
A common production pattern is assembling a query or log message from optional parts:
StringBuilder sb = new StringBuilder(256); sb.append("SELECT * FROM orders WHERE 1=1"); if (status != null) { sb.append(" AND status = '").append(status).append('\''); } if (minPrice != null) { sb.append(" AND price >= ").append(minPrice); } String query = sb.toString();
This is a typical use case: the final string is assembled from optional parts, and the number of parts is not known until runtime. The pre-sized constructor avoids reallocation when the query grows, and the conditional append calls keep the code readable without introducing intermediate strings.