Mutable vs Immutable Strings
The Core Distinction
Immutable means once a string object is created, its content cannot change. Every "modification" — appending, replacing, upper-casing — creates a brand new string object in memory. The original is untouched.
Mutable means the string's content can be changed in place. Characters can be overwritten, new characters can be inserted, and the same object is modified without allocating a new one.
This distinction is not just academic. It determines whether a loop that builds a string runs in O(n) or O(n²). It determines whether you can modify a character at a specific index. It determines which APIs are available and what the thread-safety guarantees are.
Immutability in action:
s = "hello" ← creates object A at address 1000
s = s + " world" ← creates object B at address 2000
("hello world" — 11 chars copied)
s = s.toUpperCase() ← creates object C at address 3000
("HELLO WORLD" — 11 chars copied)
Object A: "hello" ← still in memory until garbage collected
Object B: "hello world" ← still in memory until garbage collected
Object C: "HELLO WORLD" ← current value of s
Three allocations, three copies, just to get "HELLO WORLD".
With n such operations: O(n²) total characters copied.
Language Summary
| Language | Default String | Mutable Alternative | Notes |
|---|---|---|---|
| Java | String — immutable | StringBuilder (single-threaded), StringBuffer (thread-safe) | Always use StringBuilder in interviews |
| Python | str — immutable | list of chars, then "".join() | No dedicated mutable string class |
| C++ | std::string — mutable | Already mutable — no alternative needed | Can modify chars directly |
| JavaScript | string — immutable primitive | Array of chars, then .join("") | Same pattern as Python |
C++ is the outlier. std::string is directly mutable — you can write s[0] = 'H' and it works. Every other major language treats strings as immutable and provides a separate mutable buffer for building strings efficiently.
Java: String vs StringBuilder
String is immutable. StringBuilder is a mutable character buffer. This difference shows up constantly in interviews.
1public class StringVsStringBuilder {
2
3 // Demonstrates the fundamental API difference
4 public static void main(String[] args) {
5 // --- Immutable String ---
6 String s = "Hello";
7
8 // Every method returns a NEW String — original unchanged
9 String upper = s.toUpperCase();
10 String appended = s + " World";
11 String replaced = s.replace("l", "L");
12
13 System.out.println("Original s: " + s); // "Hello" — unchanged
14 System.out.println("upper: " + upper); // "HELLO"
15 System.out.println("appended: " + appended); // "Hello World"
16 System.out.println("replaced: " + replaced); // "HeLLo"
17
18 // --- Mutable StringBuilder ---
19 StringBuilder sb = new StringBuilder("Hello");
20
21 // Every method modifies the SAME object — no new allocation
22 sb.append(" World"); // Modifies sb in place
23 sb.insert(5, ","); // Insert at index 5
24 sb.delete(0, 6); // Delete characters 0-5
25 sb.replace(0, 5, "Java"); // Replace range
26 sb.reverse(); // Reverse in place
27
28 System.out.println("After operations: " + sb.toString()); // "avaJ"
29
30 // StringBuilder useful methods
31 StringBuilder builder = new StringBuilder();
32 builder.append("one");
33 builder.append(", ");
34 builder.append("two");
35 builder.append(", ");
36 builder.append("three");
37
38 System.out.println("Built string: " + builder.toString());
39 System.out.println("Length: " + builder.length());
40 System.out.println("charAt(0): " + builder.charAt(0));
41
42 // Modify a character at a specific index — impossible with String
43 builder.setCharAt(0, 'O');
44 System.out.println("After setCharAt: " + builder.toString());
45 }
46}Output (Java): Original s: Hello upper: HELLO appended: Hello World replaced: HeLLo After operations: avaJ Built string: one, two, three After setCharAt: One, two, three
The O(n²) Trap and How to Avoid It
This is the most tested practical consequence of immutability. Building a string by concatenation in a loop is one of the most common interview anti-patterns.
1public class ConcatTrap {
2
3 // BAD: O(n²) — creates a new String at every iteration
4 public static String buildSlow(int n) {
5 String result = "";
6 for (int i = 0; i < n; i++) {
7 result += (char)('a' + i % 26); // NEW String object every time
8 }
9 return result;
10 }
11
12 // GOOD: O(n) — appends to the same internal buffer
13 public static String buildFast(int n) {
14 StringBuilder sb = new StringBuilder(n); // Pre-size for zero resizes
15 for (int i = 0; i < n; i++) {
16 sb.append((char)('a' + i % 26)); // In-place append
17 }
18 return sb.toString();
19 }
20
21 // ALSO BAD (subtle): String.format inside a loop
22 public static String buildFormat(int n) {
23 String result = "";
24 for (int i = 0; i < n; i++) {
25 result += String.format("%c", 'a' + i % 26); // New String every time
26 }
27 return result;
28 }
29
30 public static void main(String[] args) {
31 int n = 100_000;
32
33 long t1 = System.nanoTime();
34 String slow = buildSlow(n);
35 long time1 = (System.nanoTime() - t1) / 1_000_000;
36
37 long t2 = System.nanoTime();
38 String fast = buildFast(n);
39 long time2 = (System.nanoTime() - t2) / 1_000_000;
40
41 System.out.println("Slow (String +): " + time1 + " ms, length=" + slow.length());
42 System.out.println("Fast (StringBuilder): " + time2 + " ms, length=" + fast.length());
43 System.out.println("Speedup: ~" + (time1 / Math.max(time2, 1)) + "x");
44 }
45}Output (approximate): Slow (String +): 3200 ms length=100000 Fast (StringBuilder): 2 ms length=100000 Speedup: ~1600x
Modifying a Character at a Specific Index
Immutable strings make single-character modification non-trivial. This is a frequent interview requirement — change s[3] to a different character.
1public class ModifyCharAtIndex {
2
3 // Method 1: StringBuilder — most efficient for multiple modifications
4 public static String replaceCharSB(String s, int index, char newChar) {
5 StringBuilder sb = new StringBuilder(s);
6 sb.setCharAt(index, newChar); // O(1) — direct index write
7 return sb.toString(); // O(n) — one final copy
8 }
9
10 // Method 2: char array — same efficiency, more explicit
11 public static String replaceCharArray(String s, int index, char newChar) {
12 char[] chars = s.toCharArray(); // O(n) — copy to array
13 chars[index] = newChar; // O(1) — direct write
14 return new String(chars); // O(n) — copy back to String
15 }
16
17 // Method 3: substring concatenation — creates multiple objects, avoid
18 public static String replaceCharSub(String s, int index, char newChar) {
19 return s.substring(0, index) + newChar + s.substring(index + 1);
20 // Creates 3 new String objects — O(n) but with higher constant
21 }
22
23 public static void main(String[] args) {
24 String s = "Hello, World!";
25
26 System.out.println("Original: " + s);
27 System.out.println("Change [0] to 'h': " + replaceCharSB(s, 0, 'h'));
28 System.out.println("Change [7] to 'w': " + replaceCharArray(s, 7, 'w'));
29 System.out.println("Original still: " + s); // Unchanged
30 }
31}Output:
Original: Hello, World!
Change [0] to 'h': hello, World!
Change [7] to 'w': Hello, world!
Original still: Hello, World!
StringBuilder API Deep Dive (Java)
StringBuilder is the most important mutable string class in Java interviews. Know every method.
1public class StringBuilderAPI {
2
3 public static void main(String[] args) {
4 // Create with initial content or initial capacity
5 StringBuilder sb1 = new StringBuilder("Hello"); // Content
6 StringBuilder sb2 = new StringBuilder(100); // Capacity only, size=0
7 StringBuilder sb3 = new StringBuilder(); // Default capacity=16
8
9 StringBuilder sb = new StringBuilder("Hello, World!");
10
11 // --- Reading ---
12 System.out.println("length(): " + sb.length()); // 13
13 System.out.println("charAt(7): " + sb.charAt(7)); // 'W'
14 System.out.println("toString(): " + sb.toString()); // "Hello, World!"
15 System.out.println("substring(0,5): " + sb.substring(0, 5)); // "Hello"
16 System.out.println("indexOf('W'): " + sb.indexOf("W")); // 7
17
18 // --- Appending ---
19 sb.append('!'); // append char
20 sb.append(" How are you?"); // append String
21 sb.append(42); // append int — converts to "42"
22 sb.append(3.14); // append double
23 System.out.println("After append: " + sb);
24
25 // --- Inserting ---
26 sb.insert(0, ">>> "); // Insert at beginning
27 sb.insert(8, "[INSERTED]"); // Insert at index 8
28 System.out.println("After insert: " + sb);
29
30 // --- Deleting ---
31 sb.delete(0, 4); // Delete range [0, 4)
32 sb.deleteCharAt(0); // Delete single character
33 System.out.println("After delete: " + sb);
34
35 // --- Replacing ---
36 sb.replace(0, 5, "Howdy"); // Replace range with new content
37 System.out.println("After replace: " + sb);
38
39 // --- Modifying ---
40 sb.setCharAt(0, 'h'); // Change one character
41 sb.reverse(); // Reverse all characters in place
42 System.out.println("Reversed: " + sb);
43
44 // --- Capacity management ---
45 StringBuilder sized = new StringBuilder(1000); // Pre-size for n chars
46 System.out.println("Capacity: " + sized.capacity()); // 1000
47 // Pre-sizing avoids internal resizes when you know the approximate size
48 }
49}Output (Java): length(): 13 charAt(7): W After append: Hello, World!! How are you?42 3.14 Reversed: (reversed content)
StringBuffer vs StringBuilder (Java)
Both are mutable string buffers. The only difference is thread safety.
| Feature | StringBuilder | StringBuffer |
|---|---|---|
| Thread safety | Not thread-safe | Thread-safe (synchronized) |
| Performance | Faster | Slower (synchronization overhead) |
| When to use | Single-threaded code — all interviews | Multi-threaded shared string building |
| Available since | Java 5 | Java 1.0 |
Rule for interviews: Always use StringBuilder. Interviewers expect it. StringBuffer exists for historical reasons and concurrent scenarios that never appear in algorithm interviews.
StringBuilder for interviews:
StringBuilder sb = new StringBuilder();
sb.append("...");
sb.reverse();
sb.setCharAt(i, c);
return sb.toString();
StringBuffer only when:
Multiple threads share the same buffer AND write to it concurrently
(Almost never in interview problems)
When to Convert: The Decision Rules
This is the practical guide for making the right choice in every situation.
Keep using String when: - You are reading only (no modification needed) - You call 1-3 methods that return new strings (s.trim().toLowerCase()) - The string is used as a hash map key - You are comparing strings for equality - The string is a function parameter or return value Switch to StringBuilder (Java) or list (Python/JS) when: - You build a string character by character in a loop - You need to modify characters at specific indices - You do 4+ operations that would each create a new string - You need to reverse or rearrange characters efficiently - Performance matters and the string is large (n > 1000 characters) Use char[] (Java) when: - You need raw performance without StringBuilder overhead - You are implementing sorting or reversal in-place - Memory allocation must be minimized (competitive programming) Convert mutable → immutable when: - You need to return the result as a String - You need to use String methods (split, contains, indexOf) - You need to use the result as a hash map key - You are done building and need the final value
Common Interview Patterns Using StringBuilder
These are the most frequent patterns where StringBuilder appears in interview solutions.
1import java.util.Stack;
2
3public class StringBuilderPatterns {
4
5 // Pattern 1: Reverse a string
6 public static String reverse(String s) {
7 return new StringBuilder(s).reverse().toString();
8 // Or manually: append backward, or use char[] swap
9 }
10
11 // Pattern 2: Remove characters matching a condition
12 public static String removeVowels(String s) {
13 StringBuilder sb = new StringBuilder();
14 String vowels = "aeiouAEIOU";
15
16 for (char c : s.toCharArray()) {
17 if (vowels.indexOf(c) == -1) {
18 sb.append(c); // Only append non-vowels
19 }
20 }
21
22 return sb.toString();
23 }
24
25 // Pattern 3: Build result from processed characters
26 public static String encodeRunLength(String s) {
27 if (s.isEmpty()) return s;
28
29 StringBuilder sb = new StringBuilder();
30 int count = 1;
31
32 for (int i = 1; i <= s.length(); i++) {
33 if (i < s.length() && s.charAt(i) == s.charAt(i - 1)) {
34 count++;
35 } else {
36 sb.append(s.charAt(i - 1));
37 if (count > 1) sb.append(count);
38 count = 1;
39 }
40 }
41
42 return sb.toString();
43 }
44
45 // Pattern 4: Decode using a stack
46 public static String reverseWords(String s) {
47 String[] words = s.trim().split("\\s+");
48 StringBuilder sb = new StringBuilder();
49
50 for (int i = words.length - 1; i >= 0; i--) {
51 sb.append(words[i]);
52 if (i > 0) sb.append(" ");
53 }
54
55 return sb.toString();
56 }
57
58 public static void main(String[] args) {
59 System.out.println("reverse: " + reverse("hello"));
60 System.out.println("removeVowels: " + removeVowels("Hello World"));
61 System.out.println("runLength 'aabbccc': " + encodeRunLength("aabbccc"));
62 System.out.println("reverseWords: " + reverseWords("the quick brown fox"));
63 }
64}Output:
reverse: olleh
removeVowels: Hll Wrld
runLength aabbccc: ab2c3
reverseWords: fox brown quick the
Interview Questions
Q: In Java, what is the difference between String, StringBuilder, and StringBuffer?
String is immutable — every modification creates a new object. StringBuilder is a mutable character buffer — modifications happen in place, and it uses a doubling strategy like a dynamic array. StringBuilder is not thread-safe. StringBuffer is identical to StringBuilder but synchronized — every method is locked, making it safe for concurrent access at the cost of performance. In interview problems, always use StringBuilder. StringBuffer is a legacy class for multi-threaded use cases that rarely appear in algorithm interviews.
Q: Why does s += "x" in a loop cause O(n²) performance in Java but not in C++?
In Java, String is immutable. Each += creates a new String object containing all previous characters plus the new one. For n iterations, total copies are 0 + 1 + 2 + ... + n-1 = O(n²). In C++, std::string is mutable. += appends to the same internal buffer using a doubling strategy (like vector::push_back). Occasional resizes copy all elements, but across n appends the total copy work is O(n) — same as dynamic array. Python lists used with join() are O(n) for the same reason.
Q: How do you reverse a string in Java without using StringBuilder.reverse()?
Convert to a char[] using toCharArray(), use two-pointer swapping (left and right converge inward, swapping characters), then construct a new String from the modified array. This is O(n) time and O(n) space. Alternatively, build a StringBuilder, append characters by iterating backward, and call toString(). Both approaches are O(n).
Q: When should you choose char[] over StringBuilder in Java?
char[] is slightly faster than StringBuilder because it avoids the StringBuilder's method call overhead and capacity checks. Use char[] when you know the exact final size upfront (no dynamic growth needed), when you are implementing a sorting or in-place manipulation algorithm, or in competitive programming where constant factors matter. For general-purpose string building in interviews, StringBuilder is the clearer choice.
FAQs
Does Python's += on strings create a new object every time?
Semantically yes — s += "x" is equivalent to s = s + "x", creating a new string. However, CPython has an optimization: when a string variable is the only reference to its object, += may extend the existing buffer in place rather than allocating a new one. This makes simple repeated += faster in practice than the worst case suggests. But this optimization is implementation-specific and not guaranteed. For production code and interviews, use the list + join pattern for loop-based string building to guarantee O(n) behavior.
What is the capacity of a StringBuilder when created with new StringBuilder()?
The default capacity in Java is 16 characters. The buffer doubles when capacity is exceeded — the same strategy as ArrayList. Creating with new StringBuilder(n) pre-allocates capacity for n characters, avoiding all resizes if you add at most n characters. Pre-sizing is worth doing in interviews when you know the approximate output length, as it avoids the O(n) resize work entirely.
Can you use a String as a HashMap key after converting it from a StringBuilder?
Yes. Once you call sb.toString(), you get an immutable String object suitable as a HashMap key. Its hash code is computed and cached on first use. You cannot use a StringBuilder directly as a HashMap key — it is mutable, so its hash could change after insertion, making it unfindable.
Is there a performance difference between sb.append('a') and sb.append("a") in Java?
Yes — append(char) is slightly faster than append(String) because it appends one character directly. append(String) requires checking the string length and possibly entering the multi-character copy path. For single characters in tight loops, prefer append((char)('a' + i)) over append(String.valueOf((char)('a' + i))).
Quick Quiz
Question 1: In Java, what is the output of: String s = "hello"; s.toUpperCase(); System.out.println(s);
- ›A)
"HELLO" - ›B)
"hello"—toUpperCase()returns a new String but s is not reassigned - ›C) Compilation error
- ›D)
"Hello"
Answer: B) "hello". toUpperCase() returns a new immutable String object. The original s is never reassigned, so it still holds "hello". To update s, you must write s = s.toUpperCase().
Question 2: You need to build a string by appending 1,000,000 characters in a loop in Java. Which is correct?
- ›A)
String result = ""; for (...) result += c;— O(n²), avoid - ›B)
StringBuilder sb = new StringBuilder(); for (...) sb.append(c); return sb.toString();— O(n), correct - ›C) Both have the same performance
- ›D)
String.format()inside the loop — O(1) per call
Answer: B) StringBuilder. String += in a loop is O(n²). StringBuilder.append() is amortized O(1) per call, giving O(n) total. String.format() creates a new String object on each call — same O(n²) problem as +=.
Question 3: In Python, what is the efficient way to build a string from a list of n words?
- ›A)
result = ""; for word in words: result += word + " " - ›B)
result = " ".join(words)— O(total characters), one allocation - ›C)
result = str(words)— converts list to string representation - ›D)
result = words[0]; for w in words[1:]: result = result + " " + w
Answer: B) " ".join(words). join() computes the total output size first, makes one allocation, and copies all characters once — O(total characters). Both A and D use repeated +=, which creates a new string at each step — O(n²).
Question 4: In C++, does s += "x" inside a loop cause the same O(n²) problem as Java?
- ›A) Yes —
std::stringis immutable like Java's String - ›B) No —
std::stringis mutable,+=uses amortized O(1) append - ›C) It depends on the string length
- ›D) Yes — C++ strings create new objects on every modification
Answer: B) No. C++ std::string is mutable. += appends to the same internal buffer using a capacity-doubling strategy. Each push_back equivalent is amortized O(1). Total for n appends is O(n). C++ avoids the immutability trap that Java and Python face.
Summary
Mutable and immutable strings serve different purposes. Immutable strings are safe to share, hash, and compare. Mutable buffers are efficient for building and modifying.
The key ideas to carry forward:
- ›In Java, Python, and JavaScript, every string "modification" creates a new string — O(n) per operation
- ›Building a string with
+in a loop is O(n²) in Java, Python, and JavaScript — a critical interview anti-pattern - ›Use
StringBuilderin Java for loop-based string building — O(n) total - ›Use
list+"".join()in Python for the same purpose - ›Use
array+.join('')in JavaScript - ›C++
std::stringis mutable —+=is amortized O(1), no special buffer needed - ›
StringBuilderandStringBufferare identical except thread safety — useStringBuilderin interviews - ›Modifying a character at index
irequires converting to a mutable structure first (except C++) - ›Pre-sizing
StringBuilderwithnew StringBuilder(n)avoids all resize copies when you know the output length
In the next topic, you will explore Time and Space Complexity of Strings — the complete complexity reference for every string operation and algorithm.