In this topic, we will talk about the StringBuilder class, which provides a mutable sequence of characters and offers an opportunity to efficiently perform various string manipulation operations in Kotlin.
Mutable strings
In Kotlin, String objects are immutable, meaning that once created, they cannot be changed. While immutability has its benefits in terms of stability, it can also introduce certain challenges when it comes to string manipulation and concatenation. Every time we perform operations like concatenation using the + operator or the plus() function, a new string object is created, which can result in unnecessary memory allocations and performance overhead.
To address these challenges, Kotlin has a special class named StringBuilder that represents a mutable sequence of characters. It can be used to efficiently perform multiple string manipulation operations, allowing you to modify its contents without creating a new object. This can help reduce the memory overhead and improve performance.
In the above image, we demonstrate different ways of concatenating two strings. First, we used the + operator on String objects, which allocates new memory to store the combined string.
In the second one, we used a StringBuilder object and its append() method (we will talk more about it in a while) to perform the concatenation. This time, no new memory was allocated. Instead, the original value was mutated to represent the combined result.
val or var when declaring variables. Immutability refers to certain data types that cannot be changed in memory once they have been created. On the other hand, val or var defines if a variable can be reassigned or not.Constructors of StringBuilder
There are two main ways to construct a StringBuilder object in Kotlin: using the StringBuilder constructor and using the buildString function.
1. Using the StringBuilder constructor
The StringBuilder class provides several constructors that allow you to create a new StringBuilder object. For example, you can create an empty StringBuilder with the default initial capacity like this:
val sb = StringBuilder()
You can also specify the initial capacity of the StringBuilder by passing an integer value to the constructor:
val sb = StringBuilder(100)
In addition, you can create a StringBuilder that contains the same characters as a given CharSequence or String by passing it to the constructor:
val sb1 = StringBuilder("Kotlin")
val sb2 = StringBuilder(sb1)
2. Using the buildString function
One of the features that make Kotlin stand out is its support for functional programming. That is why a more convenient way to construct a StringBuilder is by using the buildString function. This function takes a lambda as an argument and allows you to build a string using a StringBuilder without having to explicitly create one. Here is an example:
val str = buildString {
append("Hello")
append(' ')
append("Kotlin")
}
println(str) // Output: Hello Kotlin
We can also pass an initial capacity to the lambda expression as an argument:
val str = buildString(100) {
// some code here
}
It is important to note that the buildString function returns a String, not a StringBuilder.
buildString can be used to achieve many of the same results as using a StringBuilder directly, there may be some cases where using a StringBuilder directly provides more flexibility or control over the string manipulation process.Methods of StringBuilder
StringBuilder provides several methods for manipulating strings, which allow you to easily modify the contents of a StringBuilder object. Let's take a closer look at some of them.
- The
appendmethod: it allows you to add characters or strings to the end of aStringBuilder. This method is overloaded to accept different types of arguments.
val str = buildString {
append("Kotlin v") // Appending a String
append(1.8) // Appending a Double
}
println(str) // Output: Kotlin v1.8
- The
insertmethod: it allows you to insert characters or strings at a specific position in aStringBuilder. This method is also overloaded to accept different types of arguments.
val str = buildString {
append("Hello World")
insert(5, ",")
}
println(str) // Output: Hello, World
- The
deletemethod: it allows you to remove characters or strings from a specific range in aStringBuilder. This method takes two arguments: the start index (included) and the end index (excluded) of the range to delete.
val str = buildString {
append("That's impossible!")
delete(7, 9)
}
println(str) // Output: That's possible!
- The
replacemethod: it allows you to replace characters or strings within a specific range in aStringBuilder. This method takes three arguments: the start index (included), the end index of the range to replace (excluded), and the string to replace with.
val str = buildString {
append("one, two, two, ...")
replace(10, 13, "three")
}
println(str) // Output: one, two, three, ...
- The
reversemethod: it allows you to reverse the characters in aStringBuilder. This method takes no arguments and reverses the entire sequence of characters.
val str = buildString {
append("Hello, Kotlin!")
reverse()
}
println(str) // Output: !niltoK ,olleH
You can find more methods in the documentation.
Length vs. capacity
The StringBuilder class has two important members: length and capacity(). The length property represents the number of characters a StringBuilder object contains, while the capacity method returns the number of characters it can hold before it needs to be resized.
Here is an example:
val sb = StringBuilder()
println("Length: ${sb.length}, Capacity: ${sb.capacity()}") // Output: Length: 0, Capacity: 16
sb.append("Hello!")
println("Length: ${sb.length}, Capacity: ${sb.capacity()}") // Output: Length: 6, Capacity: 16
The capacity of a StringBuilder is the number of characters it can hold before it needs to be resized, and it is always greater than or equal to its length. When you create a new StringBuilder, its initial capacity is determined by the constructor you use. If you use the default constructor, the initial capacity will be characters. If you pass a string when constructing a StringBuilder, it will add the length of that string to the initial capacity.
As you append new characters to a StringBuilder, its length increases. If the length exceeds the current capacity, the StringBuilder automatically increases its capacity to accommodate the additional characters. The new capacity is calculated using the following formula: newCapacity = max(oldCapacity * 2 + 2, newLength).
Here is an example that demonstrates how the capacity of a StringBuilder created with the default constructor changes as characters are added:
val sb = StringBuilder() // Capacity = initial capacity = 16
println("Capacity: ${sb.capacity()}") // Output: Capacity: 16
sb.append("0123456789abcdef") // (oldCapacity >= newLength) then: newCapacity = oldCapacity
println("Capacity: ${sb.capacity()}") // Output: Capacity: 16
sb.append('g') // // (oldCapacity < newLength) then: newCapacity = max(oldCapacity * 2 + 2, newLength) = oldCapacity * 2 + 2
println("Capacity: ${sb.capacity()}") // Output: Capacity: 34
And here is an example with a StringBuilder created by passing a string to its constructor:
val sb = StringBuilder("Kotlin!") // Capacity = initial capacity + "Kotlin!".length = 16 + 7
println("Capacity: ${sb.capacity()}") // Output: Capacity: 23
sb.append("0123456789abcdef") // (oldCapacity >= newLength) then: newCapacity = oldCapacity
println("Capacity: ${sb.capacity()}") // Output: Capacity: 23
sb.append("0123456789abcdef0123456789abcdef") // (oldCapacity < newLength) then: newCapacity = max(oldCapacity * 2 + 2, newLength) = newLength
println("Capacity: ${sb.capacity()}, Length: ${sb.length}") // Output: Capacity: 55Best practices
Here are some best practices for using the StringBuilder class:
-
Use
StringBuilderfor complex string manipulation tasks.StringBuilderis well-suited for complex string manipulation tasks, such as building or modifying a string incrementally. For simple string concatenation tasks, it may be more efficient to use the+operator or string interpolation. -
Pre-allocate the capacity of the
StringBuilder. If you know the approximate size of the final string, you can pre-allocate the capacity of theStringBuilderto reduce the number of times it needs to be resized. This can help improve performance by reducing the number of memory allocations. -
Reuse the same
StringBuilderobject. If you need to perform multiple string manipulation operations, consider reusing the sameStringBuilderobject rather than creating a new one each time. This can help reduce memory overhead and improve performance. To do that, you can reset theStringBuilderto an empty state by calling itssetLengthmethod with an argument of . This will clear the contents of theStringBuilderwithout changing its capacity, allowing you to reuse it for further string manipulation operations. Here is an example:val sb = StringBuilder() sb.append("Java") println(sb.toString()) // Output: Java sb.setLength(0) sb.append("Kotlin") println(sb.toString()) // Output: Kotlin -
Convert a
StringBuilderto aStringwhen finished. Once you have finished manipulating aStringBuilder, you may want to convert it to aString. This can be done easily by calling thetoStringmethod on theStringBuilderobject. Keep in mind that calling this method creates a newStringobject, so if you need to perform further string manipulation operations, it may be more efficient to continue using theStringBuilder.
Also, here are some best practices for using the buildString function:
-
Use
buildStringfor simple string construction. ThebuildStringfunction is well-suited for simple string construction tasks, such as concatenating a small number of strings or building a string in a loop. For more complex string manipulation tasks, it may be more efficient to use aStringBuilderdirectly. -
Avoid unnecessary object creation. When using the
buildStringfunction, avoid creating unnecessary objects, such as temporary strings or character arrays. Instead, use the methods available on theStringBuilderto manipulate the string directly. -
Leverage the scope of the lambda. The
buildStringfunction takes a lambda as an argument, which provides a convenient scope for building the string. Use this scope to your advantage by declaring local variables and functions that can help simplify your code.
Here is an example that demonstrates these best practices:
val numbers = listOf(1, 2, 3, 4, 5)
val str = buildString {
for (number in numbers) {
append(number)
append(' ')
}
}
println(str) // Output: 1 2 3 4 5
In the example, we use the buildString function to construct a string by appending numbers and spaces in a loop. We avoid creating unnecessary objects by using the append method to manipulate the string directly. We also leverage the scope of the lambda by declaring a local variable (number) that simplifies our code.
Conclusion
In this topic, we've learned about StringBuilder, a useful tool for efficiently performing multiple string manipulation operations in Kotlin. We've explored the differences between StringBuilder and String and learned how to construct and manipulate StringBuilder objects using methods such as append, insert, and delete.
We've also learned about the buildString function, which provides a convenient way to construct a String using a StringBuilder without having to explicitly create one. We discussed best practices for using both StringBuilder and buildString, including pre-allocating the capacity of the StringBuilder, reusing the same StringBuilder object, and converting a StringBuilder to a String when finished.
Applying these concepts, you can use StringBuilder and buildString to effectively manipulate strings in your Kotlin programs. But first, let's put your knowledge into practice.