StringBuffer is a mutable sequence of characters in Java. Like StringBuilder, it allows text to be changed without creating a new immutable String for every modification. The important difference is that StringBuffer's commonly used mutation methods are synchronized, making the class designed for use where thread safety is required.
If StringBuilder is your fast workbench for assembling text in ordinary single-threaded code, StringBuffer is the workbench with built-in synchronization for situations where multiple threads may access the same mutable text object.
Why StringBuffer Exists
Java applications sometimes have multiple threads working with shared mutable data. A mutable character sequence can become unsafe when several threads modify the same object at the same time.
StringBuffer was introduced as a thread-safe mutable alternative for character sequences. Its methods such as append(), insert(), delete(), and replace() are synchronized.
Remember the three-way distinction: String is immutable, StringBuilder is mutable and generally preferred for ordinary single-threaded construction, and StringBuffer is mutable with synchronized methods.
Creating a StringBuffer
You can create an empty StringBuffer using its no-argument constructor.
StringBuffer buffer = new StringBuffer();
buffer.append("Java");
System.out.println(buffer);
Output:
Java
You can also create one with initial text.
StringBuffer buffer = new StringBuffer("Java");
System.out.println(buffer);
Output:
Java
Appending Text
The append() method adds content to the end of the existing character sequence.
StringBuffer buffer = new StringBuffer();
buffer.append("Java");
buffer.append(" ");
buffer.append("Programming");
System.out.println(buffer);
Output:
Java Programming
The same buffer object is modified after each append operation.
Appending Different Types
StringBuffer provides overloaded append methods for many primitive and reference types.
StringBuffer buffer = new StringBuffer();
buffer.append("Age: ");
buffer.append(25);
buffer.append(", Active: ");
buffer.append(true);
buffer.append(", Score: ");
buffer.append(95.5);
System.out.println(buffer);
Output:
Age: 25, Active: true, Score: 95.5
Method Chaining
StringBuffer methods that modify the buffer generally return the same StringBuffer object, allowing method chaining.
StringBuffer buffer = new StringBuffer();
buffer.append("Java")
.append(" ")
.append("StringBuffer");
System.out.println(buffer);
Output:
Java StringBuffer
Chaining is convenient, but readability should always come first.
insert()
The insert() method adds content at a specified position.
StringBuffer buffer = new StringBuffer("Java Programming");
buffer.insert(5, "String ");
System.out.println(buffer);
Output:
Java String Programming
Indexes are zero-based, so position 5 identifies the location immediately after the original word "Java" and its following space.
delete()
The delete() method removes a range of characters.
StringBuffer buffer = new StringBuffer("Java Programming");
buffer.delete(5, 16);
System.out.println(buffer);
Output:
Java
The start index is inclusive and the end index is exclusive.
deleteCharAt()
Use deleteCharAt() when a single character needs to be removed.
StringBuffer buffer = new StringBuffer("Jvaa");
buffer.deleteCharAt(2);
System.out.println(buffer);
Output:
Java
replace()
The replace() method replaces characters within a specified range.
StringBuffer buffer = new StringBuffer("Java Programming");
buffer.replace(5, 16, "Development");
System.out.println(buffer);
Output:
Java Development
The original range is replaced with the supplied text, which does not have to be the same length as the removed range.
reverse()
The reverse() method reverses the character sequence.
StringBuffer buffer = new StringBuffer("Java");
buffer.reverse();
System.out.println(buffer);
Output:
avaJ
The operation modifies the existing buffer rather than creating a separate mutable object for the reversed sequence.
charAt() and setCharAt()
StringBuffer allows you to read and modify individual characters.
StringBuffer buffer = new StringBuffer("Java");
System.out.println(buffer.charAt(0));
buffer.setCharAt(0, 'K');
System.out.println(buffer);
Output:
J Kava
This is another clear example of mutability. The character sequence can be changed directly.
length()
The length() method returns the number of characters currently stored in the buffer.
StringBuffer buffer = new StringBuffer("Java");
System.out.println(buffer.length());
Output:
4
The length changes as content is appended, inserted, replaced, or deleted.
toString()
When an API or method requires a String, convert the StringBuffer using toString().
StringBuffer buffer = new StringBuffer();
buffer.append("Java");
buffer.append(" Programming");
String result = buffer.toString();
System.out.println(result);
Output:
Java Programming
StringBuffer is mutable, but toString() gives you a String representation that you can use as normal immutable text.
StringBuffer Capacity
StringBuffer maintains an internal character buffer and therefore has both a current length and a capacity.
StringBuffer buffer = new StringBuffer();
System.out.println("Length: " + buffer.length());
System.out.println("Capacity: " + buffer.capacity());
The initial capacity is larger than the initial length. When more characters are added than the current capacity can hold, the implementation expands the internal storage.
You can also specify an initial capacity when creating the object.
StringBuffer buffer = new StringBuffer(100); System.out.println(buffer.capacity());
An appropriate initial capacity can be useful when you have a reasonable estimate of how much text will be generated.
ensureCapacity()
The ensureCapacity() method requests enough internal capacity for at least the specified number of characters.
StringBuffer buffer = new StringBuffer(); buffer.ensureCapacity(1000); System.out.println(buffer.capacity());
This is mainly useful when the expected size of the generated text is known or can be estimated.
What Makes StringBuffer Thread-Safe?
The key feature of StringBuffer is synchronization. Its mutating and access methods are synchronized, which provides built-in coordination when the same buffer is accessed by multiple threads.
For example, suppose two threads share the same StringBuffer and both append text. Synchronization helps ensure that individual synchronized method calls are not simultaneously modifying the buffer's internal state.
StringBuffer buffer = new StringBuffer();
buffer.append("Thread-A");
buffer.append(" Thread-B");
System.out.println(buffer);
The important point is not that StringBuffer magically makes every multi-step operation atomic. Synchronization applies to its synchronized method calls. If several operations must be treated as one indivisible business operation, additional synchronization or another concurrency design may still be necessary.
Thread-safe does not mean every sequence of multiple operations is automatically atomic. Always consider the complete operation your application needs to protect.
StringBuffer in Multi-Threaded Code
Consider a shared buffer accessed by multiple threads:
StringBuffer buffer = new StringBuffer();
Thread first = new Thread(() -> {
buffer.append("First ");
});
Thread second = new Thread(() -> {
buffer.append("Second ");
});
first.start();
second.start();
Both threads can safely invoke the synchronized append operation on the shared StringBuffer. However, the order in which the text appears depends on thread scheduling, so thread safety should not be confused with a guaranteed execution order.
StringBuffer vs StringBuilder
This is one of the most important comparisons in this topic. Both classes provide mutable character sequences and expose very similar APIs. Their major practical difference is synchronization.
| Feature | StringBuilder | StringBuffer |
|---|---|---|
| Mutable | Yes | Yes |
| Common use | General text construction | Shared mutable text where synchronization is required |
| Methods synchronized | No | Yes, for its relevant methods |
| Synchronization overhead | Generally lower | Generally higher |
| Preferred for ordinary single-threaded code | Usually | Usually not |
| API similarity | Very high | Very high |
In modern Java development, StringBuilder is usually the natural choice when one thread owns the builder. StringBuffer is more specialized and is appropriate when its synchronization characteristics are actually useful.
StringBuffer vs String
String and StringBuffer solve different problems.
| Feature | String | StringBuffer |
|---|---|---|
| Mutability | Immutable | Mutable |
| Can modify existing object | No | Yes |
| Thread-safe by immutability | Yes for sharing immutable content | Provides synchronized mutable operations |
| Best suited for | Stable text values | Mutable text requiring synchronized access |
If the text never needs to change, String is normally the simpler and clearer choice. If the text changes repeatedly and synchronization is needed around the mutable character sequence, StringBuffer may be appropriate.
When Should You Use StringBuffer?
Do not choose StringBuffer simply because it sounds safer. Choose it when the synchronization provided by the class fits the actual concurrency requirements of your design.
- Use StringBuffer when a mutable character sequence is shared between threads and its synchronized methods are suitable for the required access pattern.
- Use StringBuilder for ordinary mutable text construction when the builder is not shared across threads.
- Use String when the value represents stable, immutable text.
A Practical Example
Suppose an application maintains a shared text log that multiple worker threads append to. A StringBuffer can provide synchronized append operations.
StringBuffer log = new StringBuffer();
log.append("Application started").append(System.lineSeparator());
log.append("Worker initialized").append(System.lineSeparator());
log.append("Task completed");
System.out.println(log);
For a real logging system, you would normally use a dedicated logging framework rather than manually sharing a StringBuffer. The example is useful for understanding the class, not as a recommendation to build production logging infrastructure this way.
Common Beginner Mistakes
- Assuming StringBuffer is always better because it is synchronized.
- Using StringBuffer for simple local string construction where StringBuilder would be clearer and typically more appropriate.
- Assuming synchronization guarantees a specific order between threads.
- Assuming a sequence of several synchronized method calls is automatically one atomic operation.
- Forgetting that StringBuffer is mutable and can therefore introduce shared-state side effects.
- Using StringBuffer when a simple immutable String is all the application needs.
Best Practices
- Prefer String for immutable text values.
- Prefer StringBuilder for ordinary mutable text construction.
- Choose StringBuffer when synchronized mutable character-sequence operations are genuinely required.
- Do not rely on StringBuffer to establish business-level ordering between threads.
- Keep shared mutable state to a minimum because concurrency becomes easier to reason about when fewer objects are shared.
- Convert the completed buffer to a String with toString() when an immutable result is required.
Interview Insight
A classic interview question is: “What is the difference between StringBuilder and StringBuffer?” The strongest short answer is that both are mutable character sequences with similar APIs, but StringBuffer's relevant methods are synchronized while StringBuilder's are not. Therefore, StringBuilder is generally preferred for ordinary single-threaded construction, while StringBuffer is intended for cases where synchronized mutable access is useful.
A follow-up question may ask whether StringBuffer automatically makes a complete multi-step operation thread-safe. The correct answer is no. Its individual synchronized methods provide method-level synchronization, but application-level atomicity may still require additional coordination.
StringBuffer at a Glance
| Requirement | StringBuffer Feature | Example |
|---|---|---|
| Add content | append() | buffer.append("Java") |
| Insert content | insert() | buffer.insert(0, "Hello ") |
| Remove a range | delete() | buffer.delete(0, 5) |
| Remove one character | deleteCharAt() | buffer.deleteCharAt(2) |
| Replace a range | replace() | buffer.replace(0, 4, "Java") |
| Reverse text | reverse() | buffer.reverse() |
| Modify one character | setCharAt() | buffer.setCharAt(0, 'K') |
| Get final String | toString() | buffer.toString() |
| Check internal capacity | capacity() | buffer.capacity() |
Final Takeaway
StringBuffer gives Java a mutable character sequence with synchronized operations, making it useful when shared mutable text requires built-in synchronization. Its API closely resembles StringBuilder, so the real decision is not about learning two completely different classes—it is about understanding when synchronization matters. In ordinary code, StringBuilder is usually the simpler choice; StringBuffer earns its place when synchronized mutable access is part of the actual design. With that distinction clear, the next step is to compare String and StringBuilder directly and understand which one should be used in different situations.
