StringBuilder in Java

  • Last Updated: February 1, 2025
  • By: javahandson
  • Series
img

StringBuilder in Java



StringBuilder in Java is a mutable sequence of characters. You can append, insert, delete and reverse text inside one object, and the object never gets replaced. That single idea makes it the fastest, cleanest way to build strings in a loop, and this guide walks through its constructors, methods, internals and traps.

1. Introduction

Think of a String as a printed page. Once the ink dries, you cannot edit it. Adding a word means printing a whole new page and throwing the old one away.

A StringBuilder is a whiteboard instead. Write on it, wipe a bit off, squeeze a word into the middle. The board stays the same board the entire time.

That is the whole difference, and it drives every other detail in this article. Java gave us StringBuilder in Java 5 as the fast, unsynchronized companion to the much older StringBuffer.

Almost every time you build text piece by piece, StringBuilder is the right tool. Let us see exactly why, and how to drive it well.

1.1 What This Article Covers

  • Why immutable String makes loop concatenation slow, and how a mutable buffer fixes it
  • All four constructors, plus the capacity each one hands you
  • Length and capacity, two numbers beginners constantly confuse
  • Every method you will reach for: append, insert, delete, replace, reverse and friends
  • The growable array inside the class, and the doubling formula that resizes it
  • Where thread safety breaks, and what to do about it
  • A side-by-side table covering String, StringBuilder and StringBuffer
  • Pitfalls that bite real code, from equals to null appends
  • A hands-on walkthrough and a dozen interview questions with answers

[IMAGE PLACEHOLDER: Diagram comparing a String creating three separate objects during concatenation against one StringBuilder object being edited in place]

2. Why StringBuilder Exists

2.1 String Never Changes

Java marks String as immutable. No method on a String edits the characters it holds.

Methods such as concat(), toUpperCase() and replace() look like they change something. They do not. Each one builds a brand new String and hands it back.

String name = "Java";
name.concat(" HandsOn");
System.out.println(name); // Output: Java

name = name.concat(" HandsOn");
System.out.println(name); // Output: Java HandsOn

The first concat() call created a new String and dropped it immediately. Only the reassignment on line four kept the result.

2.2 The Loop That Quietly Burns Memory

Immutability costs almost nothing for a couple of joins. Put it inside a loop and the bill arrives fast.

String result = "";
for (int i = 0; i < 50000; i++) {
    result += "x";   // a brand new String on every single pass
}
System.out.println(result.length()); // Output: 50000

Every pass allocates a fresh String and copies all the characters gathered so far. Pass one copies 1 character, pass two copies 2, and pass fifty thousand copies 49,999.

Add those copies together and you get roughly 1.25 billion character copies. Computer scientists call this O(n2) work, and your CPU calls it a bad afternoon.

2.3 The Mutable Alternative

Swap the String for a StringBuilder and the copying disappears. One buffer grows a handful of times, and each character lands in it exactly once.

StringBuilder builder = new StringBuilder();
for (int i = 0; i < 50000; i++) {
    builder.append("x");   // same object, edited in place
}
String result = builder.toString();
System.out.println(result.length()); // Output: 50000

Same answer, but the work drops to O(n). On a typical machine the first version takes seconds while this one finishes in a couple of milliseconds.

Notice the final toString() call. A builder is a workspace, not a finished value, so hand back a String once the building is done.

3. The Four Constructors

Four constructors exist, and they differ in one thing only: how much room the buffer starts with.

3.1 The Empty Builder

new StringBuilder() gives you an empty builder with room for 16 characters.

StringBuilder sb = new StringBuilder();
System.out.println(sb.length());   // Output: 0
System.out.println(sb.capacity()); // Output: 16

Sixteen is simply the default the JDK picked. Nothing magic about it.

3.2 The Sized Builder

Pass an int and you choose the starting room yourself.

StringBuilder sb = new StringBuilder(100);
System.out.println(sb.length());   // Output: 0
System.out.println(sb.capacity()); // Output: 100

Roughly knowing the final size? Then this constructor saves a few resizes. Passing a negative number throws NegativeArraySizeException.

3.3 The Builder From a String

Give it a String and the builder starts with that content. Capacity becomes the string length plus 16.

StringBuilder sb = new StringBuilder("Java");
System.out.println(sb.length());   // Output: 4
System.out.println(sb.capacity()); // Output: 20  (4 + 16)

Those spare 16 slots are deliberate. Most code appends right after creating the builder, so the JDK leaves elbow room.

3.4 The Builder From a CharSequence

The last constructor accepts any CharSequence, which covers String, StringBuffer, another StringBuilder, and CharBuffer.

StringBuffer buffer = new StringBuffer("Java HandsOn");
StringBuilder sb = new StringBuilder(buffer);

System.out.println(sb);            // Output: Java HandsOn
System.out.println(sb.capacity()); // Output: 28  (12 + 16)

Copying a StringBuffer into a StringBuilder like this is a common way to shed synchronization once a value stops being shared.

4. Length vs Capacity

4.1 Two Numbers That Mean Different Things

Beginners mix these two up constantly, so let us pin them down.

  • length() counts the characters you actually put in
  • capacity() counts the slots currently reserved in memory

Picture a bus with 50 seats carrying 12 passengers. Length is 12 and capacity is 50.

Only length affects what toString() prints. Capacity is a memory detail that changes on its own.

4.2 Watching Capacity Grow

Overflow the reserved room and the builder allocates a bigger array, copies everything across, and carries on.

StringBuilder sb = new StringBuilder(4);
System.out.println(sb.capacity()); // Output: 4

sb.append("Hello");                // needs 5 slots, only 4 exist
System.out.println(sb.length());   // Output: 5
System.out.println(sb.capacity()); // Output: 10  (4 * 2 + 2)

Your code never triggers that copy by hand. The builder handles it silently, which is exactly why it feels effortless to use.

4.3 ensureCapacity and trimToSize

Two methods let you take the wheel when memory matters.

ensureCapacity(int) reserves room up front. Ask for less than you already have and nothing happens at all.

StringBuilder sb = new StringBuilder();
System.out.println(sb.capacity()); // Output: 16

sb.ensureCapacity(50);
System.out.println(sb.capacity()); // Output: 50

sb.ensureCapacity(10);             // already bigger, so ignored
System.out.println(sb.capacity()); // Output: 50

trimToSize() does the opposite. It shrinks the array down to the characters you are holding, releasing whatever went unused.

StringBuilder sb = new StringBuilder(50);
sb.append("JavaHandsOn");

System.out.println(sb.capacity()); // Output: 50
sb.trimToSize();
System.out.println(sb.capacity()); // Output: 11

Reach for trimToSize() only when a builder will sit in memory for a long time. Trimming a short-lived builder just wastes a copy.

5. The Methods You Will Actually Use

5.1 append

This is the workhorse. Roughly nine out of ten builder calls in real code are appends.

Overloads cover every primitive plus Object, String, CharSequence and char arrays. Anything that is not already text goes through String.valueOf() first.

StringBuilder sb = new StringBuilder("Java");

sb.append(" HandsOn");   // String
sb.append(' ');          // char
sb.append(2026);         // int
sb.append(' ');
sb.append(true);         // boolean

System.out.println(sb); // Output: Java HandsOn 2026 true

One small habit pays off. Prefer append('x') over append("x") for a single character, because the char overload skips a String lookup.

5.2 insert

Where append writes at the end, insert() writes at an index you choose. Everything after that index slides right.

StringBuilder sb = new StringBuilder("Java HandsOn");

sb.insert(0, ">> ");
System.out.println(sb); // Output: >> Java HandsOn

sb.insert(sb.length(), " <<");
System.out.println(sb); // Output: >> Java HandsOn <<

Valid offsets run from 0 to length() inclusive. Anything outside that range throws StringIndexOutOfBoundsException.

Keep in mind that inserting near the front shifts every later character. Appending stays cheap no matter how big the builder grows.

5.3 delete and deleteCharAt

delete(start, end) removes a range. Start is included and end is excluded, matching substring().

StringBuilder sb = new StringBuilder("Java HandsOn");

sb.delete(4, 10);       // removes " Hands"
System.out.println(sb); // Output: JavaOn

deleteCharAt(index) drops exactly one character. Handy for stripping a trailing comma.

StringBuilder sb = new StringBuilder("Java,");

sb.deleteCharAt(sb.length() - 1);
System.out.println(sb); // Output: Java

An end index past the length is forgiven by delete(), which simply stops at the end. A start index past the length still throws.

5.4 replace and setCharAt

replace(start, end, str) swaps a range for new text. The replacement can be any length, so the builder grows or shrinks to fit.

StringBuilder sb = new StringBuilder("Java Hello");

sb.replace(5, 10, "HandsOn");
System.out.println(sb); // Output: Java HandsOn

setCharAt(index, ch) overwrites a single character in place. It returns nothing, unlike the chainable methods.

StringBuilder sb = new StringBuilder("java");

sb.setCharAt(0, 'J');
System.out.println(sb); // Output: Java

5.5 reverse

Flipping the character order takes one call, which is why interviewers love it.

String word = "HandsOn";
String flipped = new StringBuilder(word).reverse().toString();

System.out.println(flipped); // Output: nOsdnaH

Surrogate pairs stay intact too. Java stores emoji and rarer scripts as two chars, and reverse() keeps those pairs glued together instead of scrambling them.

5.6 Reading the Buffer

Editing is only half the job. These methods read without changing anything.

StringBuilder sb = new StringBuilder("Java HandsOn Java");

System.out.println(sb.charAt(5));            // Output: H
System.out.println(sb.indexOf("Java"));      // Output: 0
System.out.println(sb.indexOf("Java", 1));   // Output: 13
System.out.println(sb.lastIndexOf("Java"));  // Output: 13
System.out.println(sb.indexOf("Python"));    // Output: -1
System.out.println(sb.substring(5, 12));     // Output: HandsOn

Watch the return type of substring(). It hands back a String, not a StringBuilder, so you cannot keep chaining builder methods off it.

A missing search term always yields -1. Test for that before using the value as an index.

5.7 setLength and Emptying a Builder

setLength(int) forces the character count. Shrinking truncates, and growing pads with the null character.

StringBuilder sb = new StringBuilder("Java HandsOn");

sb.setLength(4);
System.out.println(sb);          // Output: Java

sb.setLength(6);                 // pads with two null characters
System.out.println(sb.length()); // Output: 6

Those padding characters are real and invisible. Growing a builder with setLength() is rarely what you want.

The genuinely useful call is setLength(0). It empties the builder for reuse while keeping the capacity you already paid for.

StringBuilder sb = new StringBuilder(64);
sb.append("first row");

sb.setLength(0);                   // clear the content
System.out.println(sb.length());   // Output: 0
System.out.println(sb.capacity()); // Output: 64  (buffer kept)

5.8 Newer Additions Worth Knowing

Recent Java releases added two small conveniences.

isEmpty() arrived with Java 15 through the CharSequence interface. It reads better than comparing the length to zero.

StringBuilder sb = new StringBuilder();
System.out.println(sb.isEmpty()); // Output: true

sb.append("Java");
System.out.println(sb.isEmpty()); // Output: false

repeat(CharSequence, int) landed in Java 21 and appends the same text several times.

StringBuilder line = new StringBuilder();
line.repeat("-", 20);

System.out.println(line); // Output: --------------------

Running an older JDK? Then a short loop with append() does the same job.

5.9 Method Chaining

Most editing methods return the very same builder instead of a new one. That single design choice makes chaining possible.

String message = new StringBuilder("Hello")
        .append(" World")
        .insert(0, ">> ")
        .reverse()
        .toString();

System.out.println(message); // Output: dlroW olleH >>

Each call hands back the builder, so the next call applies to the updated content. Chains read well right up to about four calls.

Break longer chains into named steps. Debugging a ten-call chain is miserable, and a stack trace only points at the whole statement.

Two methods sit outside the pattern. setCharAt() and setLength() both return void, so a chain stops dead at either one.

5.10 Copying Characters Out

Sometimes you need the raw characters rather than a String. Three methods cover that.

getChars() copies a slice straight into an array you already own, which avoids allocating anything new.

StringBuilder sb = new StringBuilder("Java HandsOn");
char[] target = new char[4];

sb.getChars(5, 9, target, 0);   // copy indexes 5..8 into target
System.out.println(target);     // Output: Hand

chars() and codePoints() both hand back an IntStream, which slots neatly into stream pipelines.

StringBuilder sb = new StringBuilder("Java HandsOn");

long vowels = sb.chars()
        .filter(ch -> "aeiouAEIOU".indexOf(ch) >= 0)
        .count();

System.out.println(vowels); // Output: 4

Pick codePoints() when the text may hold emoji or non-Latin scripts. It treats a surrogate pair as one value, while chars() reports two.

6. How StringBuilder Works Inside

6.1 A Growable Array Behind the Scenes

Strip away the methods and a StringBuilder holds two fields: an array for the characters and an int for how many slots are in use.

Both StringBuilder and StringBuffer inherit those fields from a package-private parent named AbstractStringBuilder. The two classes really are twins, and synchronization is the main thing separating them.

Appending writes into the free slots and bumps the count. No allocation happens at all while spare room remains, and that is where the speed comes from.

6.2 The Growth Formula

Run out of room and the builder computes a new size:

new capacity = (old capacity * 2) + 2

Should that still fall short of what the append needs, the builder jumps straight to the required size instead.

StringBuilder sb = new StringBuilder(5);
System.out.println(sb.capacity()); // Output: 5

sb.append("Java HandsOn");         // needs 12 slots
System.out.println(sb.capacity()); // Output: 12

sb.append(" Tutorial Series");     // needs 28, doubling gives 26
System.out.println(sb.capacity()); // Output: 28

Doubling matters more than it looks. Growing by one slot each time would copy the whole array on every append, dragging you back to O(n2).

With doubling, the copies get rarer as the builder grows. Filling 50,000 characters from the default 16 takes about a dozen resizes, not 50,000.

6.3 Compact Strings Since Java 9

Java 9 changed the storage under both String and StringBuilder. The old char[] became a byte[] paired with a one-byte coder flag.

Text that fits in Latin-1 now uses one byte per character rather than two. Content needing wider characters flips the coder to UTF-16 and uses two bytes.

Most English text halves its memory footprint for free. Nothing about the API changed, and capacity() still reports characters rather than bytes.

One subtle effect is worth knowing. Appending the first non-Latin-1 character forces the builder to re-encode its whole buffer into UTF-16.

6.4 Why toString Still Copies

Calling toString() is not free. It allocates a new String and copies every character across.

Why the copy? Because String promises immutability. Sharing the live buffer would let a later append mutate a String, and the whole language leans on that never happening.

The practical rule follows directly. Call toString() once, at the end.

// Wasteful: a full copy on every pass
for (String item : items) {
    sb.append(item);
    System.out.println(sb.toString().length());
}

// Better: length() reads the buffer directly, no copy
for (String item : items) {
    sb.append(item);
    System.out.println(sb.length());
}

Reading methods such as length(), charAt() and indexOf() touch the buffer directly. None of them allocate, so use them freely inside loops.

7. Thread Safety: The Honest Version

7.1 What Goes Wrong

No method on StringBuilder is synchronized. Share one builder between threads and the results turn to nonsense.

package com.java.handson.strings;

public class UnsafeBuilderDemo {

    public static void main(String[] args) throws InterruptedException {

        StringBuilder shared = new StringBuilder();

        Runnable job = () -> {
            for (int i = 0; i < 10000; i++) {
                shared.append('x');
            }
        };

        Thread one = new Thread(job);
        Thread two = new Thread(job);

        one.start();
        two.start();
        one.join();
        two.join();

        System.out.println(shared.length()); // Output: 17342 (expected 20000)
    }
}

Run it and the number changes every time. It almost never reaches 20,000.

Here is the reason. Both threads read the same character count, both write into the same slot, and one write silently overwrites the other.

Worse outcomes are possible. Two threads resizing at once can throw ArrayIndexOutOfBoundsException from deep inside the JDK, which is a confusing stack trace to debug.

7.2 Two Ways to Fix It

The first fix wraps the shared builder in a lock. Every thread must hold the same lock before touching it.

Runnable job = () -> {
    for (int i = 0; i < 10000; i++) {
        synchronized (shared) {
            shared.append('x');
        }
    }
};
// Output: 20000, every run

The second fix removes the sharing instead. Give each thread its own builder and merge the pieces at the end.

StringBuilder local = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    local.append('x');
}
// hand `local` back and combine the results once every thread finishes

Prefer the second approach whenever you can. It needs no locks, no contention, and no reasoning about interleaving.

What about StringBuffer? Its locking protects one method call, never a sequence of them. A check-then-append across two calls stays broken even with StringBuffer, so a shared builder usually needs your own lock anyway.

8. String vs StringBuilder vs StringBuffer

Here is the comparison interviewers ask for, in one table.

Aspect String StringBuilder StringBuffer
Mutable No Yes Yes
Thread-safe Yes, because it never changes No Yes, per method call
Speed for edits Slowest Fastest Middle
Available since Java 1.0 Java 5 Java 1.0
Stored in the String pool Yes, for literals No No
equals() compares Content Identity Identity
Safe as a HashMap key Yes No No
Best used for Fixed text and keys Building text in one thread A buffer genuinely shared across threads

One rule covers nearly every case. Use String for values, StringBuilder for building, and StringBuffer only when threads truly share the same buffer.

[IMAGE PLACEHOLDER: Decision flowchart asking "Does the text change?" then "Do multiple threads share it?" leading to String, StringBuilder or StringBuffer]

9. Common Mistakes and Pitfalls

9.1 Calling equals on Two Builders

StringBuilder never overrides equals(). It inherits the identity check from Object, so two builders holding identical text still compare as different.

StringBuilder a = new StringBuilder("Java");
StringBuilder b = new StringBuilder("Java");

System.out.println(a.equals(b));                       // Output: false
System.out.println(a.toString().equals(b.toString())); // Output: true
System.out.println(a.compareTo(b));                    // Output: 0  (Java 11+)

Convert to String before comparing content. The same gap applies to hashCode(), which is why a StringBuilder makes a terrible map key.

9.2 Appending a null

Appending a null String reference does not throw. It quietly writes the four letters n-u-l-l into your text.

String missing = null;
StringBuilder sb = new StringBuilder("Name: ");

sb.append(missing);
System.out.println(sb);          // Output: Name: null
System.out.println(sb.length()); // Output: 10

One overload behaves differently. Passing a null char[] throws NullPointerException instead, so guard those values before appending.

9.3 Mixing Concatenation Back In

This one hides in plain sight and undoes the whole point of using a builder.

// Wasteful: builds a throwaway String on every pass
for (String item : items) {
    sb.append(item + ", ");
}

// Better: two direct appends, no intermediate String
for (String item : items) {
    sb.append(item).append(", ");
}

The first loop concatenates before appending, creating a temporary String each time. Chained appends skip that entirely.

9.4 Off-by-One Index Errors

Range methods follow the same rule as String: start is included, end is excluded.

  • delete(0, 3) removes indexes 0, 1 and 2, leaving index 3 alone
  • Removing the last character needs deleteCharAt(sb.length() - 1)
  • Valid insert offsets stretch from 0 to length() inclusive
  • Bad indexes raise StringIndexOutOfBoundsException, a subclass of IndexOutOfBoundsException

9.5 Reaching for a Builder You Do Not Need

Plenty of code creates a builder where plain concatenation is clearer and just as fast.

// Overkill for a single join
String label = new StringBuilder().append("Hi ").append(name).toString();

// Clear, and the compiler optimises it anyway
String label = "Hi " + name;

Since Java 9, the compiler turns a simple + expression into an efficient runtime call through StringConcatFactory. Manual builders win in loops, not in one-liners.

9.6 Treating toString as a Live View

A String pulled out of a builder is a snapshot. Later edits to the builder leave that String untouched.

StringBuilder sb = new StringBuilder("Java");
String snapshot = sb.toString();

sb.append(" HandsOn");

System.out.println(sb);       // Output: Java HandsOn
System.out.println(snapshot); // Output: Java

Beginners often expect snapshot to follow along. It cannot, because String never changes once created.

The same trap runs the other way too. Storing a builder in a list and editing it afterwards changes what the list holds, which surprises people who expected String behaviour.

10. Hands-On Walkthrough

10.1 Building a Small Report

Let us pull the pieces together into one program that formats an order summary.

package com.java.handson.strings;

import java.util.List;

public class ReportBuilder {

    public static void main(String[] args) {

        List<String> items = List.of("Keyboard", "Monitor", "Mouse");

        StringBuilder report = new StringBuilder(128);

        report.append("ORDER SUMMARY").append('\n');
        report.append("-------------").append('\n');

        for (int i = 0; i < items.size(); i++) {
            report.append(i + 1)
                  .append(". ")
                  .append(items.get(i))
                  .append('\n');
        }

        report.append("Total items: ").append(items.size());

        System.out.println(report);
        System.out.println("Capacity used: " + report.length() + " of " + report.capacity());
    }
}
// Output:
// ORDER SUMMARY
// -------------
// 1. Keyboard
// 2. Monitor
// 3. Mouse
// Total items: 3
// Capacity used: 74 of 128

Three decisions in that program are worth calling out.

  • Sizing the builder at 128 avoided every resize, since the report never grew past 74 characters
  • Chaining inside the loop kept each row on one readable statement
  • Passing the builder straight to println() worked because print calls toString() for you

10.2 The Trailing Separator Trick

Joining values with commas always leaves one comma too many. Two lines clean it up.

List<String> items = List.of("Keyboard", "Monitor", "Mouse");

StringBuilder csv = new StringBuilder();
for (String item : items) {
    csv.append(item).append(", ");
}

System.out.println(csv); // Output: Keyboard, Monitor, Mouse,

if (csv.length() > 0) {
    csv.setLength(csv.length() - 2);   // drop the trailing ", "
}

System.out.println(csv); // Output: Keyboard, Monitor, Mouse

Notice the length guard. Running setLength() on an empty builder would ask for a negative length and throw.

For plain joining, String.join(", ", items) is shorter and does this for you. Keep the trick for cases where you build rows conditionally and cannot use a ready-made joiner.

10.3 Writing a Clean toString Method

Every class you write eventually needs a toString(). This is where StringBuilder earns its keep in everyday code.

package com.java.handson.strings;

public class Course {

    private final String title;
    private final int lessons;
    private final boolean free;

    public Course(String title, int lessons, boolean free) {
        this.title = title;
        this.lessons = lessons;
        this.free = free;
    }

    @Override
    public String toString() {
        return new StringBuilder(64)
                .append("Course{title='").append(title)
                .append("', lessons=").append(lessons)
                .append(", free=").append(free)
                .append('}')
                .toString();
    }

    public static void main(String[] args) {
        System.out.println(new Course("Java Basics", 24, true));
    }
}
// Output: Course{title='Java Basics', lessons=24, free=true}

Every append here uses a different overload. Strings, an int, a boolean and a char all flow into the same buffer without you converting anything by hand.

Honest caveat: for a field or two, a plain + expression reads better and compiles to something just as fast. Bring in the builder once the field list grows or the format turns conditional.

10.4 When to Use a Joiner Instead

Java ships two helpers built on top of StringBuilder, and both beat hand-rolled separator logic.

List<String> items = List.of("Keyboard", "Monitor", "Mouse");

// StringJoiner handles the prefix, separator and suffix
StringJoiner joiner = new StringJoiner(", ", "[", "]");
items.forEach(joiner::add);
System.out.println(joiner); // Output: [Keyboard, Monitor, Mouse]

// Collectors.joining does the same inside a stream
String csv = items.stream().collect(Collectors.joining(", "));
System.out.println(csv);    // Output: Keyboard, Monitor, Mouse

So when does a raw builder still win? Whenever the output is not a simple join.

  • Rows that mix separators, line breaks and headings
  • Text assembled across several methods rather than one expression
  • Places where you insert, delete or reverse partway through
  • Hot loops where you reuse one buffer with setLength(0)

11. Interview Questions

Q: What is StringBuilder in Java?

A: StringBuilder is a mutable sequence of characters, added in Java 5. Methods such as append, insert and delete edit the same object instead of creating a new one, which makes it the standard way to build text in a loop.

Q: What is the difference between StringBuilder and StringBuffer?

A: Both are mutable and share the same API, because both extend AbstractStringBuilder. StringBuffer synchronizes its methods, so it is thread-safe but slower. StringBuilder skips the locking, which makes it faster and the right default for single-threaded code.

Q: Why is StringBuilder faster than String concatenation in a loop?

A: String is immutable, so each concatenation allocates a new String and copies every character collected so far. That gives O(n squared) work. StringBuilder writes into one growable buffer and copies only during occasional resizes, giving O(n) work.

Q: What is the default capacity of a StringBuilder?

A: An empty StringBuilder starts with capacity 16. Building one from a String or CharSequence gives capacity equal to that content length plus 16, so new StringBuilder("Java") reports 20.

Q: How does StringBuilder grow its capacity?

A: When an append needs more room, the builder computes old capacity times two plus two. If that is still too small, it uses the exact size the append requires. Doubling keeps resizes rare, so appending stays cheap on average.

Q: What is the difference between length() and capacity()?

A: length() counts the characters actually stored, while capacity() counts the slots reserved in memory. Only length affects the output of toString(). Capacity is a memory detail that grows automatically as you append.

Q: Is StringBuilder thread-safe?

A: No. None of its methods are synchronized. Two threads appending to one shared StringBuilder can lose characters or throw ArrayIndexOutOfBoundsException. Give each thread its own builder, or guard the shared one with your own lock.

Q: Why does equals() return false for two StringBuilders with the same text?

A: StringBuilder does not override equals() or hashCode(), so both fall back to Object identity. Call toString() on each one and compare the results, or use compareTo() which Java 11 added for content ordering.

Q: How do you clear a StringBuilder?

A: Call setLength(0). That empties the content while keeping the capacity you already allocated, which is ideal when you reuse the builder in a loop. Creating a new StringBuilder also works, but it throws away the buffer.

Q: Can StringBuilder be used as a HashMap key?

A: Avoid it. It uses identity hashing, so a lookup with an equal-looking builder misses. Its content can also change after insertion, which strands the entry in the wrong bucket. Convert to String and use that as the key.

Q: Does the compiler already use StringBuilder for the + operator?

A: Older compilers rewrote plus expressions into StringBuilder calls. Since Java 9, javac emits an invokedynamic call to StringConcatFactory instead, which the runtime optimises. Either way the rewrite happens per expression, so a plus inside a loop still allocates on every pass.

Q: What does trimToSize() do?

A: It shrinks the internal array so the capacity matches the current length, releasing unused memory. Use it only for builders that live a long time, because trimming a short-lived builder costs an extra array copy for no benefit.

12. Conclusion

Let us wrap up what we covered. StringBuilder gives you a mutable character buffer, so building text no longer means throwing away a String on every step.

We started with the immutability that makes loop concatenation expensive. From there we walked the constructors, separated length from capacity, and used every method you will meet in real code.

We also opened the class up. Inside sits a growable array that doubles plus two, backed since Java 9 by compact byte storage.

Five points are worth carrying away:

  • Editing methods change the same object and return it, so chaining comes naturally
  • Capacity starts at 16, or content length plus 16, and grows by doubling plus two
  • No method is synchronized, so never share one builder across threads without a lock
  • Both equals() and hashCode() compare identity, so call toString() before comparing text
  • Simple one-line joins belong to the + operator, while loops belong to a builder

Make StringBuilder your default whenever text gets assembled piece by piece. Save StringBuffer for the rare buffer that threads genuinely share.

13. Further Reading

This article belongs to our String series on javahandson.com. Read them in order for the full picture:

You may also enjoy final Keyword in Java, which explains the modifier that locks the StringBuilder class down, and HashMap in Java, which shows exactly why a mutable key causes so much trouble.

Leave a Comment