Chapter 9: Strings and Character Processing
9.03Beginner / Intermediate7 topics14 min7 interactive animations

String building

StringBuilder is the mutable counterpart of String for building and editing text: append, insert, delete, reverse, capacity growth and why repeated concatenation in a loop is quadratic.

Learning goals

  • Use StringBuilder for loops that build text
  • Know the cost of insert, delete and reverse
  • Explain capacity growth
  • Quantify the cost of repeated concatenation

Prerequisites

Core operationsDynamic arrays
9.03.1

StringBuilder

StringBuilder holds a mutable character buffer (a dynamic array of chars) and edits it in place. Create one, call append repeatedly, and finish with toString(). It is not thread-safe; StringBuffer is the synchronised, slower variant that you rarely need.

Appending characters to a builder grows its buffer like a dynamic array, which the next animation shows.

Java
1StringBuilder sb = new StringBuilder();
2sb.append("id=").append(42).append(';').append(true); // chaining works because append returns the builder
3String s = sb.toString(); // "id=42;true"
4int len = sb.length(); // 10

StringBuilder capacity growth (2n + 2)

Interactive · edit the input

Your input

Try

characters in the builder · length 0

UsedSpare capacity

value[] · capacity 4

0
1
2
3
Setup

new StringBuilder(4) allocates a buffer of 4 chars. Appending never copies until the buffer is full.

Step 1 / 32
Java
1// StringBuilder.append(char), simplified
2void append(char c) {
3 if (count + 1 > value.length) { // no room for one more char
4 value = Arrays.copyOf(value, 2 * value.length + 2); // JDK 17 growth rule
5 }
6 value[count++] = c;
7}

Live state

0 copies0 resizes

count

0

capacity

4

No condition evaluated on this step
Execution trace (dry run table)Show 32 steps
StepLineStateWhat happens
1L2count=0 capacity=4new StringBuilder(4) allocates a buffer of 4 chars. Appending never copies until the buffer is full.
2L3count=0 capacity=4 c='b'Room left: write 'b' directly.
3L6count=1 capacity=4value[0] = 'b'; count = 1.
4L3count=1 capacity=4 c='u'Room left: write 'u' directly.
5L6count=2 capacity=4value[1] = 'u'; count = 2.
6L3count=2 capacity=4 c='i'Room left: write 'i' directly.
7L6count=3 capacity=4value[2] = 'i'; count = 3.
8L3count=3 capacity=4 c='l'Room left: write 'l' directly.
9L6count=4 capacity=4value[3] = 'l'; count = 4.
10L3count=4 capacity=4 c='d'5 > 4: the buffer is full.
11L4count=4 capacity=10New capacity 2 x 4 + 2 = 10; 4 chars copied (4 copied in total).
12L6count=5 capacity=10value[4] = 'd'; count = 5.
13L3count=5 capacity=10 c='e'Room left: write 'e' directly.
14L6count=6 capacity=10value[5] = 'e'; count = 6.
15L3count=6 capacity=10 c='r'Room left: write 'r' directly.
16L6count=7 capacity=10value[6] = 'r'; count = 7.
17L3count=7 capacity=10 c='p'Room left: write 'p' directly.
18L6count=8 capacity=10value[7] = 'p'; count = 8.
19L3count=8 capacity=10 c='a'Room left: write 'a' directly.
20L6count=9 capacity=10value[8] = 'a'; count = 9.
21L3count=9 capacity=10 c='t'Room left: write 't' directly.
22L6count=10 capacity=10value[9] = 't'; count = 10.
23L3count=10 capacity=10 c='t'11 > 10: the buffer is full.
24L4count=10 capacity=22New capacity 2 x 10 + 2 = 22; 10 chars copied (14 copied in total).
25L6count=11 capacity=22value[10] = 't'; count = 11.
26L3count=11 capacity=22 c='e'Room left: write 'e' directly.
27L6count=12 capacity=22value[11] = 'e'; count = 12.
28L3count=12 capacity=22 c='r'Room left: write 'r' directly.
29L6count=13 capacity=22value[12] = 'r'; count = 13.
30L3count=13 capacity=22 c='n'Room left: write 'n' directly.
31L6count=14 capacity=22value[13] = 'n'; count = 14.
32L7length=14 capacity=22 resizes=2 copies=14 return=length 14, capacity 2214 chars appended with 2 resizes and 14 chars copied. Capacity roughly doubles each time, so appends are amortised O(1). Pass an expected size to the constructor to avoid resizes.
9.03.2

Append

append accepts any type and writes at the end: amortised O(1) per character because the buffer grows geometrically. It is the right operation for building output in a loop.

Append the characters one by one and watch the capacity.

Java
1StringBuilder sb = new StringBuilder();
2for (int i = 0; i < 5; i++) sb.append(i).append(' ');
3String line = sb.toString().trim(); // "0 1 2 3 4"
4sb.setLength(0); // reuse the builder: length 0, capacity unchanged

StringBuilder capacity growth (2n + 2)

Interactive · edit the input

Your input

Try

characters in the builder · length 0

UsedSpare capacity

value[] · capacity 4

0
1
2
3
Setup

new StringBuilder(4) allocates a buffer of 4 chars. Appending never copies until the buffer is full.

Step 1 / 32
Java
1// StringBuilder.append(char), simplified
2void append(char c) {
3 if (count + 1 > value.length) { // no room for one more char
4 value = Arrays.copyOf(value, 2 * value.length + 2); // JDK 17 growth rule
5 }
6 value[count++] = c;
7}

Live state

0 copies0 resizes

count

0

capacity

4

No condition evaluated on this step
Execution trace (dry run table)Show 32 steps
StepLineStateWhat happens
1L2count=0 capacity=4new StringBuilder(4) allocates a buffer of 4 chars. Appending never copies until the buffer is full.
2L3count=0 capacity=4 c='b'Room left: write 'b' directly.
3L6count=1 capacity=4value[0] = 'b'; count = 1.
4L3count=1 capacity=4 c='u'Room left: write 'u' directly.
5L6count=2 capacity=4value[1] = 'u'; count = 2.
6L3count=2 capacity=4 c='i'Room left: write 'i' directly.
7L6count=3 capacity=4value[2] = 'i'; count = 3.
8L3count=3 capacity=4 c='l'Room left: write 'l' directly.
9L6count=4 capacity=4value[3] = 'l'; count = 4.
10L3count=4 capacity=4 c='d'5 > 4: the buffer is full.
11L4count=4 capacity=10New capacity 2 x 4 + 2 = 10; 4 chars copied (4 copied in total).
12L6count=5 capacity=10value[4] = 'd'; count = 5.
13L3count=5 capacity=10 c='e'Room left: write 'e' directly.
14L6count=6 capacity=10value[5] = 'e'; count = 6.
15L3count=6 capacity=10 c='r'Room left: write 'r' directly.
16L6count=7 capacity=10value[6] = 'r'; count = 7.
17L3count=7 capacity=10 c='p'Room left: write 'p' directly.
18L6count=8 capacity=10value[7] = 'p'; count = 8.
19L3count=8 capacity=10 c='a'Room left: write 'a' directly.
20L6count=9 capacity=10value[8] = 'a'; count = 9.
21L3count=9 capacity=10 c='t'Room left: write 't' directly.
22L6count=10 capacity=10value[9] = 't'; count = 10.
23L3count=10 capacity=10 c='t'11 > 10: the buffer is full.
24L4count=10 capacity=22New capacity 2 x 10 + 2 = 22; 10 chars copied (14 copied in total).
25L6count=11 capacity=22value[10] = 't'; count = 11.
26L3count=11 capacity=22 c='e'Room left: write 'e' directly.
27L6count=12 capacity=22value[11] = 'e'; count = 12.
28L3count=12 capacity=22 c='r'Room left: write 'r' directly.
29L6count=13 capacity=22value[12] = 'r'; count = 13.
30L3count=13 capacity=22 c='n'Room left: write 'n' directly.
31L6count=14 capacity=22value[13] = 'n'; count = 14.
32L7length=14 capacity=22 resizes=2 copies=14 return=length 14, capacity 2214 chars appended with 2 resizes and 14 chars copied. Capacity roughly doubles each time, so appends are amortised O(1). Pass an expected size to the constructor to avoid resizes.
9.03.3

Insert

insert(index, x) puts text in the middle: the tail must shift right by the length of the inserted text, so it costs O(n - index + inserted). Repeated insert(0, ...) builds a string in O(n²); append and reverse() at the end, or use a Deque, instead.

Choose insert in the editor animation and compare moving a long tail with inserting at the end.

Java
1StringBuilder sb = new StringBuilder("abcdef");
2sb.insert(2, "XY"); // "abXYcdef": the tail 'cdef' moved right by 2
3sb.insert(sb.length(), "!"); // same as append: nothing moves

StringBuilder insert, delete and reverse

Interactive · edit the input

Your input

Try

StringBuilder contents · length 6

a
b
c
d
e
f
012345
Tail being shiftedInserted / swappedBeing deleted
Update

Insert "XY" at 2: the 4 chars from index 2 on must move right by 2.

Step 1 / 2
Java
1sb.insert(index, str); // shifts the tail right by str.length(): O(n - index + str)
2sb.delete(start, end); // shifts the tail left; end is exclusive: O(n - start)
3sb.reverse(); // two pointers swap from both ends: O(n)
4sb.deleteCharAt(sb.length() - 1); // removing the last char shifts nothing: O(1)

Live state

0 chars moved

index

2

str.length()

2

tail

4

No condition evaluated on this step
Execution trace (dry run table)Show 2 steps
StepLineStateWhat happens
1L1index=2 str.length()=2 tail=4Insert "XY" at 2: the 4 chars from index 2 on must move right by 2.
2L1return="abXYcdef" chars moved=4Result "abXYcdef". Moved 4 chars: cost grows with the tail, so repeated insert(0, ...) is O(n²).
9.03.4

Delete

delete(start, end) removes the range [start, end) and shifts the tail left; deleteCharAt(i) removes one character; setLength(n) truncates. Deleting near the end is cheap and deleting near the front moves almost everything: O(n - start).

Remove a range in the animation and check the moved-characters counter.

Java
1StringBuilder sb = new StringBuilder("abcdef");
2sb.delete(1, 4); // "aef": removed "bcd"
3sb.deleteCharAt(sb.length() - 1); // "ae": O(1), nothing to shift
4sb.setLength(1); // "a"

StringBuilder insert, delete and reverse

Interactive · edit the input

Your input

Try

StringBuilder contents · length 6

a
b
c
d
e
f
012345
Tail being shiftedInserted / swappedBeing deleted
Update

Insert "XY" at 2: the 4 chars from index 2 on must move right by 2.

Step 1 / 2
Java
1sb.insert(index, str); // shifts the tail right by str.length(): O(n - index + str)
2sb.delete(start, end); // shifts the tail left; end is exclusive: O(n - start)
3sb.reverse(); // two pointers swap from both ends: O(n)
4sb.deleteCharAt(sb.length() - 1); // removing the last char shifts nothing: O(1)

Live state

0 chars moved

index

2

str.length()

2

tail

4

No condition evaluated on this step
Execution trace (dry run table)Show 2 steps
StepLineStateWhat happens
1L1index=2 str.length()=2 tail=4Insert "XY" at 2: the 4 chars from index 2 on must move right by 2.
2L1return="abXYcdef" chars moved=4Result "abXYcdef". Moved 4 chars: cost grows with the tail, so repeated insert(0, ...) is O(n²).
9.03.5

Reverse

reverse() reverses the characters in place with two swapping pointers: O(n), O(1) extra space. It treats a surrogate pair as one unit, so emoji are not broken. new StringBuilder(s).reverse().toString() is the idiomatic way to reverse a string.

Reverse is also how the digits of a base conversion get into the right order.

Java
1String r = new StringBuilder("stressed").reverse().toString(); // "desserts"
2boolean pal = new StringBuilder("level").reverse().toString().equals("level"); // true

StringBuilder insert, delete and reverse

Interactive · edit the input

Your input

Try

StringBuilder contents · length 6

a
b
c
d
e
f
012345
Tail being shiftedInserted / swappedBeing deleted
Update

Insert "XY" at 2: the 4 chars from index 2 on must move right by 2.

Step 1 / 2
Java
1sb.insert(index, str); // shifts the tail right by str.length(): O(n - index + str)
2sb.delete(start, end); // shifts the tail left; end is exclusive: O(n - start)
3sb.reverse(); // two pointers swap from both ends: O(n)
4sb.deleteCharAt(sb.length() - 1); // removing the last char shifts nothing: O(1)

Live state

0 chars moved

index

2

str.length()

2

tail

4

No condition evaluated on this step
Execution trace (dry run table)Show 2 steps
StepLineStateWhat happens
1L1index=2 str.length()=2 tail=4Insert "XY" at 2: the 4 chars from index 2 on must move right by 2.
2L1return="abXYcdef" chars moved=4Result "abXYcdef". Moved 4 chars: cost grows with the tail, so repeated insert(0, ...) is O(n²).
9.03.6

Capacity

A builder's capacity is the size of its internal buffer; the default is 16, and new StringBuilder(n) reserves n. When an append would exceed the capacity, the buffer grows to 2 × capacity + 2 (JDK 17) and the existing characters are copied. capacity() reports it and ensureCapacity reserves in advance. Presize when you know the final length.

Pick an initial capacity of 4 and append 14 characters in the animation.

Java
1StringBuilder sb = new StringBuilder(4);
2for (int i = 0; i < 14; i++) sb.append('x');
3int cap = sb.capacity(); // 22: 4 -> 10 -> 22
4int len = sb.length(); // 14

StringBuilder capacity growth (2n + 2)

Interactive · edit the input

Your input

Try

characters in the builder · length 0

UsedSpare capacity

value[] · capacity 4

0
1
2
3
Setup

new StringBuilder(4) allocates a buffer of 4 chars. Appending never copies until the buffer is full.

Step 1 / 32
Java
1// StringBuilder.append(char), simplified
2void append(char c) {
3 if (count + 1 > value.length) { // no room for one more char
4 value = Arrays.copyOf(value, 2 * value.length + 2); // JDK 17 growth rule
5 }
6 value[count++] = c;
7}

Live state

0 copies0 resizes

count

0

capacity

4

No condition evaluated on this step
Execution trace (dry run table)Show 32 steps
StepLineStateWhat happens
1L2count=0 capacity=4new StringBuilder(4) allocates a buffer of 4 chars. Appending never copies until the buffer is full.
2L3count=0 capacity=4 c='b'Room left: write 'b' directly.
3L6count=1 capacity=4value[0] = 'b'; count = 1.
4L3count=1 capacity=4 c='u'Room left: write 'u' directly.
5L6count=2 capacity=4value[1] = 'u'; count = 2.
6L3count=2 capacity=4 c='i'Room left: write 'i' directly.
7L6count=3 capacity=4value[2] = 'i'; count = 3.
8L3count=3 capacity=4 c='l'Room left: write 'l' directly.
9L6count=4 capacity=4value[3] = 'l'; count = 4.
10L3count=4 capacity=4 c='d'5 > 4: the buffer is full.
11L4count=4 capacity=10New capacity 2 x 4 + 2 = 10; 4 chars copied (4 copied in total).
12L6count=5 capacity=10value[4] = 'd'; count = 5.
13L3count=5 capacity=10 c='e'Room left: write 'e' directly.
14L6count=6 capacity=10value[5] = 'e'; count = 6.
15L3count=6 capacity=10 c='r'Room left: write 'r' directly.
16L6count=7 capacity=10value[6] = 'r'; count = 7.
17L3count=7 capacity=10 c='p'Room left: write 'p' directly.
18L6count=8 capacity=10value[7] = 'p'; count = 8.
19L3count=8 capacity=10 c='a'Room left: write 'a' directly.
20L6count=9 capacity=10value[8] = 'a'; count = 9.
21L3count=9 capacity=10 c='t'Room left: write 't' directly.
22L6count=10 capacity=10value[9] = 't'; count = 10.
23L3count=10 capacity=10 c='t'11 > 10: the buffer is full.
24L4count=10 capacity=22New capacity 2 x 10 + 2 = 22; 10 chars copied (14 copied in total).
25L6count=11 capacity=22value[10] = 't'; count = 11.
26L3count=11 capacity=22 c='e'Room left: write 'e' directly.
27L6count=12 capacity=22value[11] = 'e'; count = 12.
28L3count=12 capacity=22 c='r'Room left: write 'r' directly.
29L6count=13 capacity=22value[12] = 'r'; count = 13.
30L3count=13 capacity=22 c='n'Room left: write 'n' directly.
31L6count=14 capacity=22value[13] = 'n'; count = 14.
32L7length=14 capacity=22 resizes=2 copies=14 return=length 14, capacity 2214 chars appended with 2 resizes and 14 chars copied. Capacity roughly doubles each time, so appends are amortised O(1). Pass an expected size to the constructor to avoid resizes.
9.03.7

Repeated concatenation costs

Because strings are immutable, s += c in a loop builds a new string each time and copies all previous characters. Appending n characters this way writes 1 + 2 + ... + n = n(n + 1)/2 characters: O(n²). With a StringBuilder it is O(n). For 10⁵ characters that is about 5 × 10⁹ copied characters versus 10⁵.

The animation counts the characters written by each approach.

Java
1String s = "";
2for (int i = 0; i < 100_000; i++) s += 'x'; // O(n^2): minutes of copying on large n
3StringBuilder sb = new StringBuilder();
4for (int i = 0; i < 100_000; i++) sb.append('x'); // O(n)
5String t = sb.toString();

s += c versus StringBuilder.append

Interactive · edit the input

Your input

Try

characters appended so far · length 0

Just appendedAppended
Setup

Strings are immutable: s += c builds a brand new string of length k + 1, copying all k old characters first.

Step 1 / 13
Java
1String s = "";
2for (char c : text.toCharArray()) s += c; // each += allocates a new string and copies the old one
3StringBuilder sb = new StringBuilder();
4for (char c : text.toCharArray()) sb.append(c); // writes one char; occasional resize amortised away

Live state

0 s += c (chars written)0 append (chars written)
No condition evaluated on this step
Execution trace (dry run table)Show 13 steps
StepLineStateWhat happens
1L1Strings are immutable: s += c builds a brand new string of length k + 1, copying all k old characters first.
2L2k=1 s += c=1 append=1Append #1: s += c wrote 1 chars (1 in total); sb.append wrote 1 (1 in total).
3L2k=2 s += c=3 append=2Append #2: s += c wrote 2 chars (3 in total); sb.append wrote 1 (2 in total).
4L2k=3 s += c=6 append=3Append #3: s += c wrote 3 chars (6 in total); sb.append wrote 1 (3 in total).
5L2k=4 s += c=10 append=4Append #4: s += c wrote 4 chars (10 in total); sb.append wrote 1 (4 in total).
6L2k=5 s += c=15 append=5Append #5: s += c wrote 5 chars (15 in total); sb.append wrote 1 (5 in total).
7L2k=6 s += c=21 append=6Append #6: s += c wrote 6 chars (21 in total); sb.append wrote 1 (6 in total).
8L2k=7 s += c=28 append=7Append #7: s += c wrote 7 chars (28 in total); sb.append wrote 1 (7 in total).
9L2k=8 s += c=36 append=8Append #8: s += c wrote 8 chars (36 in total); sb.append wrote 1 (8 in total).
10L2k=9 s += c=45 append=9Append #9: s += c wrote 9 chars (45 in total); sb.append wrote 1 (9 in total).
11L2k=10 s += c=55 append=10Append #10: s += c wrote 10 chars (55 in total); sb.append wrote 1 (10 in total).
12L2k=11 s += c=66 append=11Append #11: s += c wrote 11 chars (66 in total); sb.append wrote 1 (11 in total).
13L4s += c=66 append=1111 appends: s += c wrote 66 chars (n(n + 1)/2, quadratic) against 11 for the builder (linear, ignoring the occasional resize).

Practice ladder

Now try it yourself

Easy

  • Build "1,2,3,4,5" with a StringBuilder in a loop
  • Reverse "abcdef" with StringBuilder

Medium

  • Why is insert(0, c) in a loop quadratic? Show a linear alternative
  • Predict the capacity after appending 20 characters to new StringBuilder(5)

Advanced

  • Measure s += c against StringBuilder.append for n = 10⁴, 10⁵ and explain the growth

Key takeaways

  • Use StringBuilder to build text in loops; append is amortised O(1).
  • insert and delete shift the tail, so near the front they are O(n).
  • Capacity grows to 2n + 2; s += c in a loop is quadratic.