StringBuilder, as you might already know, is a powerful tool that provides you with mutable strings. However, it may not be the best solution in situations where multiple threads need to modify the same StringBuilder object simultaneously. That's where StringBuffer comes in. It is a peer class to StringBuilder, with an additional feature: synchronization (a mechanism that ensures that only one thread can access a shared resource at a time, preventing race conditions and other concurrency issues). This makes StringBuffer thread-safe and a better choice for multi-threaded environments. However, there is a trade-off between synchronization and performance, which we will talk more about later on.
Constructors of StringBuffer
Similar to the case of StringBuilder, we have various ways to create a StringBuffer object. We can initialize it as an empty object, set it with a suitable initial content (String or CharSequence), or specify a starting capacity.
An empty
StringBufferwith a default initial capacity (16 bytes) can be created using the default constructor:
val buffer = StringBuffer()
You can also create a
StringBufferwith some initial content by passing aStringto the constructor:
val buffer = StringBuffer("Kotlin")
If you want to specify the capacity of the
StringBufferat the time of creation, you can pass anIntto the constructor:
val buffer = StringBuffer(10) // creates a StringBuffer with an initial capacity of 10 characters
It's worth noting that StringBuffer is a Java class. However, you can use it directly in Kotlin without importing anything, since it belongs to the java.lang package, which is automatically imported.
Methods of StringBuffer
StringBuffer shares many similar methods with StringBuilder, so we will briefly mention a few of them as a reminder. However, it's important to note that there is a crucial difference under the hood. A closer look at the StringBuffer class inside the java.lang library reveals that all StringBuffer methods are marked with the synchronized modifier. This is what makes StringBuffer synchronized and thread-safe, allowing it to be used by multiple threads without the need for external synchronization.
Now, let's review those methods:
Method | Description |
|---|---|
| Retrieves the current capacity (number of characters that can be stored without reallocation) of a |
| Retrieves the current length (number of characters) of a |
val buffer = StringBuffer("Kotlin")
println("Length: ${buffer.length}") // "Length: 6"
println("Capacity: ${buffer.capacity()}") // "Capacity: 22" (initial capacity + length of String passed to constructor = 16 + 6)
Method | Description |
|---|---|
| Adds a String at the end of a |
| Inserts a String at a specific position in a |
| Removes a range of characters from a |
| Replaces a range of characters with a new |
| Reverses the order of characters in the |
val secretMessage = StringBuffer()
secretMessage
.append("like")
.insert(3, "srepyHigh")
.delete(9, 13)
.replace(0, 1, "ll")
.reverse()
println(secretMessage) // "Hyperskill"
When using a StringBuffer to append or insert data from a source sequence that is shared across threads, it is important to ensure that the source sequence is not modified by other threads while the operation is in progress. This is because only the methods of the StringBuffer are synchronized, not the StringBuffer itself.
Proper usage of each class
It's important to choose the right class for a certain job. Depending on your specific requirements and constraints, either StringBuilder or StringBuffer might be the more suitable choice. Let's explore some real-world scenarios to help you make informed decisions.
Best scenarios for using StringBuilder:
String concatenation: If you're working on tasks that involve heavy string concatenation operations, such as generating dynamic SQL queries or constructing large JSON/XML payloads,
StringBuilderis an excellent choice. Its mutable nature allows for efficient appending of strings, reducing memory consumption and avoiding unnecessary object creations.Logging and formatting: When logging or formatting messages that involve dynamic or variable data,
StringBuilderproves beneficial. By appending different values or variables to theStringBuilder, you can efficiently build complex log messages or formatted strings without unnecessary overhead.Generating HTML or XML markup: When dynamically generating HTML or XML markup,
StringBuildercan be a suitable choice. It allows you to efficiently build the structure and content of the markup by appending tags, attributes, and values without incurring unnecessary overhead.
Best scenarios for using StringBuffer:
Concurrent data processing: When processing data concurrently in a multi-threaded environment,
StringBuffercan be used to safely manipulate shared strings. For example, if multiple threads need to extract and process data from a shared log file or data stream,StringBuffercan be used to ensure that the data is consistently and accurately processed.Multi-threaded text generation: In applications that generate text concurrently, such as report generation or document formatting,
StringBuffercan be used to safely build the final text output. By usingStringBuffer, multiple threads can append text to the shared buffer without worrying about synchronization issues.Thread-safe string caching: In applications that cache frequently used strings, such as web servers or database systems,
StringBuffercan be used to implement a thread-safe cache. By usingStringBuffer, multiple threads can safely access and manipulate the cached strings without data corruption or inconsistencies.
Converting StringBuffer to String
Converting a StringBuffer to a String allows you to obtain an immutable representation of the modified content for further processing or passing it to methods that expect a String parameter. This can be achieved using the toString method:
val buffer = StringBuffer("Hyperskill")
val string = buffer.toString()
println(string) // "Hyperskill"
The toString method returns a new String object that represents the content of the StringBuffer. Therefore, any subsequent changes made to the StringBuffer after calling toString will not affect the String object. It is important to convert a StringBuffer to a String when you no longer need to modify the content or want to ensure its immutability.
The price of synchronization
When trying to choose between StringBuilder and StringBuffer, you should consider the trade-off between synchronization and performance. Synchronization in StringBuffer comes at a cost: it introduces a slight overhead, making it slightly slower than StringBuilder. Therefore, if thread safety is not a concern in your specific scenario, it will be more efficient to use StringBuilder. It's worth noting that StringBuffer has been a part of Java since its initial release (Java ), while StringBuilder was introduced as a lightweight alternative in Java , offering improved performance for non-thread-safe operations.
So, while both classes serve the purpose of creating and manipulating mutable sequences of characters and share similar methods, there is one crucial distinction: StringBuffer is thread-safe, which enables its usage by multiple threads without requiring external synchronization. On the other hand, StringBuilder is not thread-safe and should only be used by a single thread.
Let's see the price of synchronization paid in a real example:
const val N = 99999999 // Number of iterations for the test
fun main() {
var time: Long
var executionTime: Long
println("For the same task:")
// Using StringBuffer
val buffer = StringBuffer()
time = System.currentTimeMillis() // We save the start time of the task
for (i in N downTo 1) {
buffer.append("")
}
executionTime = System.currentTimeMillis() - time // We find the execution time of the task
// by subtracting the start time from the end time
println("StringBuffer gets it finished in $executionTime ms,")
// Using StringBuilder
val builder = StringBuilder()
time = System.currentTimeMillis()
for (i in N downTo 1) {
builder.append("")
}
executionTime = System.currentTimeMillis() - time
println("while StringBuilder gets it finished in just $executionTime ms!")
}
The output of this code will look like this:
For the same task:
StringBuffer gets it finished in 1999 ms,
while StringBuilder gets it finished in just 193 ms!
In this example, we perform a test where both StringBuffer and StringBuilder are used to repeatedly append an empty string N times. We measure the execution time for each approach using the System.currentTimeMillis() method.
When you run this code, you will notice that StringBuilder performs significantly faster than StringBuffer, due to the overhead of synchronization of the latter. Please note that the actual execution times may vary based on the system and environment where the code is run.
Conclusion
When multiple threads try to access and modify the same StringBuilder object at the same time, they may cause data inconsistency or corruption. StringBuffer, on the other hand, is a synchronized and thread-safe version of StringBuilder. It ensures that only one thread can access and modify the string at a time, preventing any concurrency issues. However, this comes at a cost of performance and memory overhead. Therefore, you should use StringBuffer only when you need to share the string among multiple threads; otherwise, StringBuilder is a better option. Now it's time for you to buffer some practice.