Computer scienceProgramming languagesKotlinTypes and data structuresStrings

StringBuilder

13 minutes read

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.

Difference in memory between String object and StringBuilder object

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.

Don't confuse immutability with the use of 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.

While 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 append method: it allows you to add characters or strings to the end of a StringBuilder. 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 insert method: it allows you to insert characters or strings at a specific position in a StringBuilder. 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 delete method: it allows you to remove characters or strings from a specific range in a StringBuilder. 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 replace method: it allows you to replace characters or strings within a specific range in a StringBuilder. 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 reverse method: it allows you to reverse the characters in a StringBuilder. 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

Difference between length and capacity

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 1616 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: 55

Best practices

Here are some best practices for using the StringBuilder class:

  1. Use StringBuilder for complex string manipulation tasks. StringBuilder is 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.

  2. Pre-allocate the capacity of the StringBuilder. If you know the approximate size of the final string, you can pre-allocate the capacity of the StringBuilder to reduce the number of times it needs to be resized. This can help improve performance by reducing the number of memory allocations.

  3. Reuse the same StringBuilder object. If you need to perform multiple string manipulation operations, consider reusing the same StringBuilder object rather than creating a new one each time. This can help reduce memory overhead and improve performance. To do that, you can reset the StringBuilder to an empty state by calling its setLength method with an argument of 00. This will clear the contents of the StringBuilder without 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
  4. Convert a StringBuilder to a String when finished. Once you have finished manipulating a StringBuilder, you may want to convert it to a String. This can be done easily by calling the toString method on the StringBuilder object. Keep in mind that calling this method creates a new String object, so if you need to perform further string manipulation operations, it may be more efficient to continue using the StringBuilder.

Also, here are some best practices for using the buildString function:

  1. Use buildString for simple string construction. The buildString function 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 a StringBuilder directly.

  2. Avoid unnecessary object creation. When using the buildString function, avoid creating unnecessary objects, such as temporary strings or character arrays. Instead, use the methods available on the StringBuilder to manipulate the string directly.

  3. Leverage the scope of the lambda. The buildString function 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.

55 learners liked this piece of theory. 3 didn't like it. What about you?
Report a typo