StringBuffer in Java

  • Last Updated: January 16, 2025
  • By: javahandson
  • Series
img

StringBuffer in Java

StringBuffer in Java is the mutable cousin of String. You can append, insert, delete and reverse text inside one object, over and over, without ever allocating a replacement. Every one of its methods also carries the synchronized modifier, so two threads can share one buffer safely.

That combination makes StringBuffer in Java a slightly odd class in 2026. It solves a real problem, but its faster twin StringBuilder solves the same problem better in almost every modern codebase. This guide covers what the class does, how its capacity grows, what its thread safety actually promises, and when you should reach for it.

1. Introduction

Java freezes a String the moment you create it. Change one character and you get a whole new object. That rule keeps your code safe, but it costs you dearly when you build text piece by piece.

So Java shipped a mutable alternative on day one: StringBuffer. One object, one internal array, and methods that edit it in place.

Java 5 later added StringBuilder, an identical class minus the locking. Both survive today, which is exactly why beginners find the pair confusing. Let us clear that up properly.

1.1 What This Article Covers

  • What makes StringBuffer mutable, and how to prove it in three lines of code
  • All four constructors, and the starting capacity each one gives you
  • The difference between length and capacity, plus the real growth formula
  • Every method you will reach for: append, insert, replace, delete, reverse and friends
  • What the synchronized modifier guarantees, and the important thing it does not
  • A three-way comparison against String and StringBuilder
  • Five traps that catch beginners, from equals() to appending a null
  • A runnable program plus the interview questions you should expect

2. What Is StringBuffer?

StringBuffer is a class in java.lang that holds a growable sequence of characters. You never import it. It has lived in Java since version 1.0.

2.1 Mutable, Not Immutable

Mutable means you can change the contents after creation. Call append() and the same object now holds more text.

Think of a String as a printed page and a StringBuffer as a whiteboard. Adding a word to the page means printing a fresh page. Adding a word to the whiteboard means picking up the marker.

That distinction drives everything else in this article. It explains the speed, the capacity field, and the need for locking.

2.2 Where It Sits in the Hierarchy

StringBuffer extends a package-private class named AbstractStringBuilder, and so does StringBuilder. The two siblings therefore share almost all of their code.

  • The class implements CharSequence, so any method accepting a CharSequence takes it directly
  • Implementing Appendable lets formatting APIs such as Formatter write straight into it
  • Serialization works too, because the class implements Serializable
  • A final modifier on the class stops anyone subclassing it

Inside, a single array holds the characters. Java 8 and earlier used a char[]. Java 9 switched to a byte[] plus a coder flag, so plain ASCII text now takes half the memory. That change goes by the name Compact Strings.

2.3 Proving Mutability

Talk is cheap, so let us watch the object identity directly:

String str = "Java";
System.out.println(System.identityHashCode(str));

str += " HandsOn";                    // builds a brand new String
System.out.println(System.identityHashCode(str));   // different number

StringBuffer sb = new StringBuffer("Java");
System.out.println(System.identityHashCode(sb));

sb.append(" HandsOn");                // edits the same object
System.out.println(System.identityHashCode(sb));    // same number

System.identityHashCode() derives a number from the object identity rather than its text. The String prints two different numbers because Java allocated a second object. The buffer prints one number twice.

One object, edited in place. That is the whole pitch.

3. The Four Constructors

StringBuffer gives you four ways to start. They differ only in the initial content and the starting capacity.

3.1 The Empty Constructor

StringBuffer sb = new StringBuffer();

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

Empty content, room for 16 characters. That default suits short text and costs almost nothing.

3.2 Choosing Your Own Capacity

StringBuffer sb = new StringBuffer(50);

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

Use this one when you already know roughly how much text is coming. Section 4.3 explains why that pays off.

3.3 Starting From a String

StringBuffer sb = new StringBuffer("Java");

System.out.println(sb);            // Output: Java
System.out.println(sb.length());   // Output: 4
System.out.println(sb.capacity()); // Output: 20

Where did 20 come from? The constructor reserves the length of your text plus 16 spare slots. Four characters plus sixteen gives twenty.

Careful with nulls here. Passing a null String throws a NullPointerException rather than creating an empty buffer.

3.4 Starting From a CharSequence

CharSequence cs = new StringBuilder("Java");
StringBuffer sb = new StringBuffer(cs);

System.out.println(sb);            // Output: Java
System.out.println(sb.capacity()); // Output: 20

Same rule for capacity: content length plus 16. This overload accepts anything implementing CharSequence, which includes String, StringBuilder and another StringBuffer.

4. Length vs Capacity

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

4.1 Two Numbers, Two Meanings

  • Length counts the characters you actually put in. sb.length() returns it.
  • Capacity counts the slots the internal array currently holds. sb.capacity() returns it.

Picture a bus. Length is how many passengers sat down. Capacity is how many seats exist.

Capacity always matches or exceeds length. Those spare seats let you append without touching memory again.

StringBuffer sb = new StringBuffer();   // capacity 16, length 0
sb.append("Java");

System.out.println(sb.length());   // Output: 4
System.out.println(sb.capacity()); // Output: 16

4.2 How the Buffer Grows

Fill every seat and the buffer must find a bigger array. It allocates one, copies the old characters across, and drops the old array for the garbage collector.

The JDK picks the new size with a simple rule:

newCapacity = max( (oldCapacity * 2) + 2 , requiredLength )

Doubling keeps the number of copies low. Adding two stops a tiny buffer from crawling upward one slot at a time. Whenever your appended text overshoots even the doubled size, the JDK jumps straight to the length you need.

Watch it happen:

StringBuffer sb = new StringBuffer(10);
System.out.println(sb.capacity()); // Output: 10

sb.append("Java");
System.out.println(sb.capacity()); // Output: 10  (still room)

sb.append(" HandsOn! Learn Java in depth");
// length is now 33, and (10 * 2) + 2 = 22 falls short, so it jumps to 33
System.out.println(sb.capacity()); // Output: 33

You may read elsewhere that the buffer grows to the next power of two, or that the formula adds four. Neither claim holds. Run the snippet above and the numbers speak for themselves.

4.3 Sizing the Buffer Up Front

Every regrow costs an allocation plus a full copy. Starting from 16 and reaching 10,000 characters means roughly nine of those cycles.

Skip them when you can estimate the final size:

// 500 rows, roughly 40 characters each
StringBuffer report = new StringBuffer(500 * 40);

for (Order order : orders) {
    report.append(order.getId()).append(',').append(order.getTotal()).append('\n');
}

Already holding a buffer? Call ensureCapacity(int) to grow it once instead of repeatedly. Finished building and holding on to the object? Call trimToSize() to hand the wasted slots back.

5. The Methods You Will Actually Use

StringBuffer exposes a long API. In practice you will lean on about eight methods.

5.1 append

The workhorse. It adds text to the end and returns the same buffer, which lets you chain calls.

StringBuffer sb = new StringBuffer("Java");

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

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

Overloads exist for every primitive, for char[], for CharSequence and for Object. The Object version calls String.valueOf(), so anything at all can go in.

5.2 insert

Same idea, except you choose the position. Everything from that index onward shifts right.

StringBuffer sb = new StringBuffer("Ja va");

sb.insert(2, " HandsOn");
System.out.println(sb); // Output: Ja HandsOn va

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

Inserting near the front costs more than appending, because the JDK must shuffle every later character along.

5.3 replace and delete

Both take a half-open range. The start index counts, the end index does not.

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

StringBuffer sb2 = new StringBuffer("Java HandsOn");
sb2.delete(5, 10);          // removes indices 5,6,7,8,9
System.out.println(sb2);    // Output: Java On

sb2.deleteCharAt(4);        // removes the single space
System.out.println(sb2);    // Output: JavaOn

Notice that the replacement text need not match the range length. The buffer stretches or shrinks to fit.

5.4 reverse

This one has no equivalent on String, which makes it a favourite in coding interviews.

StringBuffer sb = new StringBuffer("Java HandsOn");
sb.reverse();
System.out.println(sb); // Output: nOsdnaH avaJ

// the one-line palindrome check
String word = "level";
boolean palindrome = word.equals(new StringBuffer(word).reverse().toString());
System.out.println(palindrome); // Output: true

It handles surrogate pairs correctly, so emoji and other supplementary characters survive the trip intact.

5.5 Reading and Resizing

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

System.out.println(sb.length());        // Output: 12
System.out.println(sb.charAt(2));       // Output: v
System.out.println(sb.indexOf("Hands"));// Output: 5
System.out.println(sb.substring(5, 12));// Output: HandsOn

sb.setCharAt(0, 'j');
System.out.println(sb);                 // Output: java HandsOn

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

Two of these deserve a warning. substring() returns a new String and leaves the buffer alone, so capture the result. And setLength() with a value above the current length pads the gap with null characters rather than spaces.

5.6 Back to a String

A buffer is a workspace, not a value. Once you finish building, hand the result over as a String.

StringBuffer sb = new StringBuffer("Java").append(" HandsOn");

String result = sb.toString();
System.out.println(result.toUpperCase()); // Output: JAVA HANDSON

// String s = sb;          // will not compile
System.out.println("as text: " + sb);     // concatenation calls toString for you

Remember that toString() copies the characters into a fresh String. Calling it inside a hot loop quietly undoes the savings the buffer gave you, so call it once at the end.

A StringBuffer never converts itself automatically. Assigning one to a String variable fails to compile, though string concatenation and println call toString() on your behalf.

5.7 Quick Method Reference

Method What it does Edits the buffer?
append(x) Adds x to the end Yes
insert(i, x) Inserts x at index i Yes
replace(s, e, str) Swaps the range [s, e) for str Yes
delete(s, e) Removes the range [s, e) Yes
deleteCharAt(i) Removes one character Yes
reverse() Flips the character order Yes
setCharAt(i, c) Overwrites one character Yes
setLength(n) Truncates, or pads with null characters Yes
length() Counts the characters present No
capacity() Counts the slots allocated No
charAt(i) Reads one character No
indexOf(str) Finds the first position of str No
substring(s, e) Returns a new String No
toString() Returns a new String No
ensureCapacity(n) Grows the array if needed Capacity only
trimToSize() Shrinks the array to the length Capacity only

6. Thread Safety: The Honest Version

Thread safety is the one thing StringBuffer offers that StringBuilder does not. It also gets oversold constantly, so read this section carefully.

6.1 Every Method Locks the Object

The JDK marks every public method on StringBuffer synchronized. Entering any of them acquires the lock on the buffer itself, and leaving it releases the lock.

So one thread at a time may run append(), insert() or even length(). Nobody can catch the internal array mid-copy and read garbage.

That safety costs you speed. Uncontended locks are cheap on a modern JVM, though never free, and Java 15 disabled biased locking by default, which removed one old optimisation for exactly this pattern.

6.2 Two Threads, One Buffer

StringBuffer sb = new StringBuffer();

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

Thread t1 = new Thread(job);
Thread t2 = new Thread(job);
t1.start();
t2.start();
t1.join();
t2.join();

System.out.println(sb.length()); // Output: 2000, every single run

Swap StringBuffer for StringBuilder and that length turns unpredictable. You might see 1,873 one run and 1,642 the next, or hit an ArrayIndexOutOfBoundsException. Two threads writing to one unguarded array corrupt it.

The order of the characters still varies with StringBuffer. Only the integrity of the buffer carries a guarantee.

6.3 The Compound Operation Trap

Here is the part the tutorials skip. Each individual call locks and unlocks. A sequence of calls does not.

// UNSAFE, even though both calls are synchronized
if (sb.length() < 100) {
    sb.append("more text");   // another thread may have appended in between
}

// SAFE: hold the lock across the whole decision
synchronized (sb) {
    if (sb.length() < 100) {
        sb.append("more text");
    }
}

The lock drops the instant length() returns. Another thread can slip in before your append() starts, and your check-then-act logic breaks.

Per-method locking never adds up to per-operation safety. When several calls must happen together, you still write your own synchronized block.

6.4 What Does the Lock Actually Cost?

Fair question. Beginners often hear “synchronized is slow” and picture a disaster. The truth sits somewhere in the middle.

int n = 5_000_000;

long t1 = System.nanoTime();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < n; i++) {
    builder.append('x');
}
long builderMs = (System.nanoTime() - t1) / 1_000_000;

long t2 = System.nanoTime();
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < n; i++) {
    buffer.append('x');
}
long bufferMs = (System.nanoTime() - t2) / 1_000_000;

System.out.println("builder ms : " + builderMs);
System.out.println("buffer ms  : " + bufferMs);

On a typical laptop the buffer takes roughly one and a half to three times as long. Both finish in tens of milliseconds, so five million appends still cost you almost nothing in human terms.

Two details shape that gap. Acquiring a lock nobody contends for is cheap, but the JIT compiler also struggles to inline and optimise across a synchronized boundary as freely as it would otherwise. Java 15 disabled biased locking by default, which removed one older trick that softened exactly this pattern.

So the honest summary reads like this. The lock rarely shows up in a profile, yet you gain nothing from paying for it on a local variable. Correctness drives the choice here far more than speed.

7. String vs StringBuilder vs StringBuffer

Three classes, one job. Here is how they line up.

7.1 The Comparison Table

Feature String StringBuilder StringBuffer
Mutable No Yes Yes
Thread-safe Yes, because it never changes No Yes, per method call
Method locking None needed None Every method synchronized
Speed for repeated edits Slowest Fastest Middle
Uses the String Pool Yes, for literals No No
equals() compares text Yes No, identity only No, identity only
Safe as a HashMap key Yes No No
Has reverse() No Yes Yes
Available since Java 1.0 Java 5 Java 1.0

7.2 So Which One Do You Pick?

  • String for values you store, return, compare or use as a map key. That covers most of your code.
  • StringBuilder whenever you assemble text, especially inside a loop. A local variable never escapes to another thread, so the missing lock costs you nothing.
  • StringBuffer only when two or more threads genuinely share one buffer object.

How often does that last case turn up? Rarely. Give each thread its own StringBuilder and the problem disappears, usually with better throughput than a shared, locked buffer.

StringBuffer still earns its keep in two places: legacy code you maintain rather than rewrite, and a genuinely shared accumulator. Everywhere else, default to StringBuilder.

8. Common Mistakes and Pitfalls

These five catch nearly everybody at least once.

8.1 Comparing Buffers With equals

StringBuffer never overrides equals(), so it inherits the version from Object. That version compares references, not characters.

StringBuffer a = new StringBuffer("Java");
StringBuffer b = new StringBuffer("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) == 0);             // Output: true, Java 11 and later

Call toString() before comparing text. Java 11 also added compareTo to StringBuffer, which compares the characters directly.

8.2 Appending null

String missing = null;
StringBuffer sb = new StringBuffer("Value: ");

sb.append(missing);
System.out.println(sb);          // Output: Value: null
System.out.println(sb.length()); // Output: 11

No exception arrives. The buffer writes the four letters n-u-l-l instead, and that text sails into your log file or your database column.

Guard the value before appending it. A quick ternary or Objects.requireNonNullElse handles it.

8.3 The char Versus int Surprise

StringBuffer sb = new StringBuffer();

sb.append('a');
System.out.println(sb); // Output: a

sb.append('a' + 1);     // 'a' + 1 promotes to int 98, so append(int) wins
System.out.println(sb); // Output: a98

sb.append((char) ('a' + 1));
System.out.println(sb); // Output: a98b

Arithmetic on a char produces an int, and Java then picks the append(int) overload. Cast back to char when you want the letter.

8.4 Using It as a HashMap Key

Two problems stack up here. StringBuffer skips hashCode() as well as equals(), so lookups fall back to identity.

Worse, the object mutates. Even a correct hash code would go stale the moment somebody appends, and your entry would vanish inside the map.

Map<String, Integer> good = new HashMap<>();
good.put(sb.toString(), 1);   // freeze it into a String first

System.out.println(good.get("Java")); // Output: 1

Always call toString() at the boundary. Keys belong to the immutable world.

8.5 Reaching for It by Default

Plenty of older tutorials present StringBuffer as the standard answer for building text. That advice predates Java 5.

Paying for a lock on a local variable that no other thread can even see is pure waste. Ask one question first: does a second thread touch this object? If the answer is no, use StringBuilder.

9. Hands-On Walkthrough

Let us fold every idea into one program you can paste and run.

9.1 The Program

package com.java.handson.strings;

public class StringBufferDemo {

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

        // 1. One object, edited in place
        StringBuffer sb = new StringBuffer("Java");
        int before = System.identityHashCode(sb);
        sb.append(" HandsOn");
        int after = System.identityHashCode(sb);
        System.out.println("1. same object   : " + (before == after));
        System.out.println("   content       : " + sb);

        // 2. Length and capacity are different numbers
        System.out.println("2. length        : " + sb.length());
        System.out.println("   capacity      : " + sb.capacity());

        // 3. Editing methods, chained
        StringBuffer edit = new StringBuffer("Java Hello");
        edit.replace(5, 10, "HandsOn").insert(0, ">> ").append("!");
        System.out.println("3. edited        : " + edit);

        // 4. reverse() has no String equivalent
        System.out.println("4. reversed      : " + new StringBuffer("Java").reverse());

        // 5. equals() compares references, not text
        StringBuffer x = new StringBuffer("Java");
        StringBuffer y = new StringBuffer("Java");
        System.out.println("5. x.equals(y)   : " + x.equals(y));
        System.out.println("   text equal    : " + x.toString().equals(y.toString()));

        // 6. append(null) writes the four letters n-u-l-l
        String missing = null;
        System.out.println("6. null appended : " + new StringBuffer().append(missing));

        // 7. Two threads sharing one buffer stay consistent
        System.out.println("7. shared length : " + sharedAppend());
    }

    private static int sharedAppend() throws InterruptedException {
        StringBuffer shared = new StringBuffer();
        Runnable job = () -> {
            for (int i = 0; i < 1000; i++) {
                shared.append('x');
            }
        };
        Thread t1 = new Thread(job);
        Thread t2 = new Thread(job);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        return shared.length();
    }
}



9.2 Reading the Output

1. same object   : true
   content       : Java HandsOn
2. length        : 12
   capacity      : 20
3. edited        : >> Java HandsOn!
4. reversed      : avaJ
5. x.equals(y)   : false
   text equal    : true
6. null appended : null
7. shared length : 2000

Every line maps back to something we covered:

  • Result 1 confirms that appending edited the original object instead of replacing it
  • Line 2 shows capacity 20 from the String constructor, which reserves your length plus 16
  • Chained calls in line 3 work because each editing method returns the buffer
  • Reversing on line 4 does the thing String simply cannot do
  • The false on line 5 is the equals() trap, sitting right next to its fix
  • Output 6 shows four letters where you probably expected an exception
  • Number 7 lands on 2000 every run, which is exactly what the locking buys you

Try one experiment. Change StringBuffer to StringBuilder inside sharedAppend() and run it ten times. Watch that last number wobble.

10. Interview Questions

Q: What is StringBuffer in Java?

A: StringBuffer is a final class in java.lang that holds a mutable sequence of characters. You can append, insert, delete, replace and reverse its contents without allocating a new object. Every public method carries the synchronized modifier, so several threads can share one buffer safely.

Q: What is the difference between StringBuffer and StringBuilder?

A: They share the same API and the same parent class. StringBuffer locks every method, which makes it thread-safe but slower. StringBuilder skips the locking, which makes it faster and unsafe across threads. Java 5 introduced StringBuilder, and it should be your default for local text building.

Q: What is the difference between length and capacity?

A: Length counts the characters actually present in the buffer. Capacity counts the slots the internal array currently holds. Capacity always matches or exceeds length, and those spare slots let you append without allocating a bigger array.

Q: What is the default capacity of a StringBuffer?

A: The no-argument constructor gives you 16 characters. The String and CharSequence constructors give you the content length plus 16, so new StringBuffer(“Java”) starts with a capacity of 20. You can also name any capacity you want with the int constructor.

Q: How does a StringBuffer grow when it runs out of room?

A: The JDK allocates a larger array, copies the characters across and discards the old one. It picks the larger of two numbers: the old capacity doubled plus two, or the length your append actually needs. It does not round up to a power of two, and it does not add four.

Q: Is StringBuffer really thread-safe?

A: Each individual method call is thread-safe, because the lock covers the whole call. A sequence of calls is not. Checking length and then appending gives another thread a window between the two, so you must wrap compound operations in your own synchronized block.

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

A: StringBuffer never overrides equals, so it inherits Object’s version, which compares references. Call toString on both and compare the resulting Strings, or use compareTo, which Java 11 added to the class.

Q: Can I use a StringBuffer as a HashMap key?

A: You should not. StringBuffer overrides neither equals nor hashCode, and its contents can change after you insert it. A mutated key lands in the wrong bucket and the entry effectively disappears. Call toString and store the immutable String instead.

Q: What happens when you append null to a StringBuffer?

A: The buffer appends the four characters n-u-l-l and throws nothing. That text then flows into your logs or your database. Check the value before you append it. Note the contrast with new StringBuffer((String) null), which does throw a NullPointerException.

Q: Can a StringBuffer give memory back after it grows?

A: Yes. Call trimToSize() and the buffer shrinks its internal array down to the current length. The array never shrinks on its own, so a buffer that briefly held a megabyte of text keeps that memory while you keep a reference to it. Setting the reference to null works too, since the garbage collector then reclaims everything.

Q: Should I still use StringBuffer in new code?

A: Rarely. Reach for it only when two or more threads share one buffer object, which almost never happens in practice. Give each thread its own StringBuilder and you get better throughput with no locking. StringBuffer mainly turns up in legacy code you maintain.

11. Conclusion

Let us wrap up what we covered. StringBuffer in Java gives you a growable character buffer you can edit in place, wrapped in a lock on every method.

We traced how it stores text in one internal array, how length differs from capacity, and how that array doubles plus two whenever you overflow it. We walked the methods you will actually reach for, and we drew the honest line around its thread safety.

Keep these five points and you will use the class correctly:

  • Editing methods change the same object and return it, which makes chaining natural
  • Capacity starts at 16, or your content length plus 16, and grows by doubling plus two
  • Per-method locking protects single calls, never a sequence of them
  • Both equals() and hashCode() compare identity, so convert to String before comparing text or using a map key
  • Choose StringBuilder unless two threads genuinely share the buffer

Learn StringBuffer for the interviews and for the legacy code you will inevitably meet. Then write StringBuilder in almost everything new.

12. 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 StringBuffer class down, and HashMap in Java, which shows exactly why a mutable key causes so much trouble.

Leave a Comment