Contents
- Why StringBuilder over String +
- append, insert, delete, replace
- indexOf, reverse, and charAt
- Capacity Management
- StringBuffer — Thread-safe Variant
String is immutable — every concatenation with + allocates a new String object and discards the old one. Inside a loop, this produces O(n²) object churn: concatenating n strings requires allocating roughly n/2 × n characters worth of temporary objects. StringBuilder solves this by maintaining a single mutable char[] buffer that grows as needed, keeping total allocation O(n). For simple one-line concatenations outside loops, the compiler already optimizes + into a StringBuilder chain — but it cannot do so across loop iterations, which is where StringBuilder is essential.
// String + in a loop — creates N intermediate String objects
// Each + is: new StringBuilder(left).append(right).toString()
String result = "";
for (int i = 0; i < 10000; i++) {
result += i; // O(n²) — very slow for large loops
}
// StringBuilder — single mutable buffer, O(n) amortized
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result2 = sb.toString(); // convert to immutable String at the end
// The compiler optimizes simple string concatenation:
// String s = "Hello" + " " + "World"; // compiler merges to one literal
// String s = a + " " + b; // compiled to StringBuilder chain
// But loop concatenation is NOT optimized by the compiler
// When + is fine (single expression, not in a loop):
String msg = "Hello, " + name + "! You have " + count + " messages."; // OK
append() adds content to the end of the buffer and returns this, enabling method chaining. insert(offset, str) inserts at a specific position, shifting existing characters right. delete(start, end) removes the characters in the half-open range [start, end); deleteCharAt(index) removes a single character. replace(start, end, str) replaces a range with a new string, which may be shorter or longer than the range it replaces. All of these methods return the StringBuilder itself, so calls can be chained freely.
StringBuilder sb = new StringBuilder("Hello");
// append — add to end (supports all types, returns this for chaining)
sb.append(", ");
sb.append("World");
sb.append('!');
sb.append(42);
sb.append(true);
System.out.println(sb); // Hello, World!42true
// Method chaining
StringBuilder chain = new StringBuilder()
.append("Java")
.append(" is ")
.append("awesome");
System.out.println(chain); // Java is awesome
// insert — insert at position
sb = new StringBuilder("Hello World");
sb.insert(5, " Beautiful"); // insert before index 5
System.out.println(sb); // Hello Beautiful World
sb.insert(0, ">>> "); // prepend
System.out.println(sb); // >>> Hello Beautiful World
// delete — remove characters [start, end)
sb = new StringBuilder("Hello Beautiful World");
sb.delete(5, 15); // removes " Beautiful"
System.out.println(sb); // Hello World
sb.deleteCharAt(0); // remove single char at index
System.out.println(sb); // ello World
// replace — replace characters [start, end) with new string
sb = new StringBuilder("Hello World");
sb.replace(6, 11, "Java");
System.out.println(sb); // Hello Java
// setCharAt — overwrite a single character
sb.setCharAt(0, 'h');
System.out.println(sb); // hello Java
indexOf(str) finds the first occurrence of a substring and returns its index, or -1 if not found; an optional offset argument starts the search further along. reverse() reverses the entire character sequence in-place — useful for palindrome checks and encoding algorithms. charAt(index) reads the character at a position; its counterpart setCharAt(index, ch) overwrites a single character without allocating anything. length() returns the number of characters currently stored, while capacity() returns the size of the underlying backing array.
StringBuilder sb = new StringBuilder("Hello World Hello");
// indexOf / lastIndexOf — find substring
System.out.println(sb.indexOf("Hello")); // 0
System.out.println(sb.lastIndexOf("Hello")); // 12
System.out.println(sb.indexOf("Hello", 1)); // 12 (start searching from index 1)
System.out.println(sb.indexOf("xyz")); // -1 (not found)
// charAt / length
System.out.println(sb.charAt(6)); // 'W'
System.out.println(sb.length()); // 17
// substring — returns a new String (does not modify sb)
System.out.println(sb.substring(6)); // "World Hello"
System.out.println(sb.substring(6, 11)); // "World"
// reverse — reverses in-place
StringBuilder rev = new StringBuilder("abcde");
rev.reverse();
System.out.println(rev); // edcba
// Palindrome check using reverse
static boolean isPalindrome(String s) {
return s.equals(new StringBuilder(s).reverse().toString());
}
System.out.println(isPalindrome("racecar")); // true
System.out.println(isPalindrome("hello")); // false
// toString — get the immutable String
String result = sb.toString();
A StringBuilder starts with a default capacity of 16 characters. When content would exceed the current capacity, it doubles (roughly) and copies the existing data to the new array. For small, unpredictable string building this is fine, but when you know the approximate final size up front, passing it to the constructor — new StringBuilder(estimatedSize) — eliminates all intermediate reallocations and copies. trimToSize() does the opposite: it shrinks the backing array to exactly the current length, releasing unused memory when the StringBuilder is done growing and will be held for a long time.
// Initial capacity is 16 chars; auto-grows when needed
StringBuilder sb = new StringBuilder(); // capacity: 16
StringBuilder sb2 = new StringBuilder(1000); // pre-allocated for 1000 chars
StringBuilder sb3 = new StringBuilder("Hi"); // capacity: 16 + "Hi".length() = 18
System.out.println(sb.capacity()); // 16
sb.append("Hello");
System.out.println(sb.capacity()); // 16 (still, 5 < 16)
sb.append("Hello World How Are You Doing"); // grows beyond 16
System.out.println(sb.capacity()); // auto-grown (typically (old+1)*2)
// ensureCapacity — guarantee at least N chars before growing
sb.ensureCapacity(500); // avoid multiple re-allocations if you know the size
// trimToSize — release unused capacity
sb = new StringBuilder(1000);
sb.append("hi");
sb.trimToSize(); // capacity trimmed to 2
// Pre-sizing for performance — if you know the final size
int n = 100_000;
StringBuilder preAllocated = new StringBuilder(n * 5); // avoid all reallocations
for (int i = 0; i < n; i++) {
preAllocated.append(i).append(',');
}
When building a string from a known number of pieces, pass an initial capacity to the StringBuilder constructor. Each time the buffer grows it must allocate a new array and copy — pre-sizing eliminates all copies.
StringBuffer has an identical API to StringBuilder, but every method is synchronized. This makes it safe to share between threads at the cost of lock acquisition on every call. In the vast majority of code, string building happens locally within a single thread, making the synchronization overhead wasteful. StringBuilder was added in Java 5 specifically to provide the unsynchronized alternative. Prefer StringBuilder by default; reach for StringBuffer only when the buffer itself is genuinely shared across threads — which is rarely the right design anyway.
// StringBuffer has the exact same API as StringBuilder
// but every method is synchronized — thread-safe but slower
StringBuffer tsf = new StringBuffer();
tsf.append("Hello").append(" World");
System.out.println(tsf.toString()); // Hello World
// When to use StringBuffer vs StringBuilder:
// - StringBuilder: use in single-threaded code (most cases) — faster
// - StringBuffer: use when the mutable string is shared across threads
// (rare — usually better to avoid shared mutable state)
// Modern alternative for thread safety: use local StringBuilder,
// then publish the final immutable String
// String is already thread-safe (immutable)
// Example: building a log line across thread-shared code
// BAD: shared StringBuffer (all threads serialize on it)
// GOOD: each thread uses its own StringBuilder, shares only the final String
// CharSequence — common interface of String, StringBuilder, StringBuffer
CharSequence cs = new StringBuilder("hello");
CharSequence cs2 = "hello"; // String also implements CharSequence
System.out.println(cs.length()); // 5
Use StringBuilder in almost all cases. StringBuffer is rarely needed — if you find yourself sharing a StringBuffer across threads, reconsider the design: usually it's better to build the string locally in each thread and then publish the final String result.