JavaRush/Courses/Kotlin SELF/StringBuilder — building long text

StringBuilder — building long text

Open

1. Introduction

When you’re just starting to program, concatenating strings with + seems perfect: you simply take pieces of text and glue them into one. And that really is fine while there are only a few pieces. But as soon as a loop appears, lots of lines, and “building a report,” it suddenly turns out that naive concatenation creates too many intermediate strings—as if you were rewriting the entire report from scratch every time, adding one letter.

String is immutable

A string (String) in Kotlin is immutable. That means: if you write val s2 = s1 + "!", then s1 didn’t “grow”; instead, a new string s2 was created. You can’t change the old string—“what’s written is written,” so to speak.

That’s convenient and safe, but sometimes expensive.

Bad pattern: += in a loop

Look at the example. It works, but there’s a catch:

fun main() {
    var text = ""

    for (i in 1..3) {
        text += "Line #$i\n"
    }

    print(text)
    // Line #1
    // Line #2
    // Line #3
}

The problem here isn’t that the code is “wrong.” The problem is that on each iteration a new text string is created, and all the old content is copied into it + the new piece. With three lines you won’t notice. With three thousand, your computer will start thinking a bit about the meaning of life.

What we want instead

We need a tool that lets us “append text inside a single object,” without recreating a giant string at every step. And this is where StringBuilder enters the scene.

2. StringBuilder: what it is and how it works

If String is “an inscription in stone,” then StringBuilder is “a notebook with a pencil.” In a notebook you can add lines one by one, erase, rearrange (within limits), and only at the end say: “Alright, now carefully copy this cleanly into a nice string.”

StringBuilder is a special class for step-by-step text assembly. It stores an internal character buffer and can efficiently extend it.

Three main stages

It helps to keep a simple picture in mind:

flowchart LR
    A["Text fragments"] --> B["StringBuilder (accumulate)"]
    B --> C["toString() (get the final String)"]

So the logic is: “accumulate” → “turn into a string” → “print/return.”

Minimal example: build a phrase

import kotlin.text.StringBuilder

fun main() {
    val sb = StringBuilder()
    sb.append("Hello")
    sb.append(", ")
    sb.append("world!")

    println(sb.toString()) // Hello, world!
}

Notice: we are not doing String + String + String. We append into a single object sb, and at the end we call toString().

String and StringBuilder in plain terms

String is a ready-made immutable string: it’s great for short values, templates like "Hello, $name", and situations where you build a string once and then only read it.

StringBuilder is a mutable “assembler”: it’s handy when you build a long text in pieces, especially in a loop, and want to get the final String at the end via toString().

3. append() and appendLine() for assembling text

When you start writing reports, you quickly realize that “append a piece of text” and “append a whole line + a newline” are two different actions. You can, of course, always add "\n" by hand, but then you’ll quickly start getting confused: where you forgot a line break, where you made two breaks in a row, where you accidentally stuck a space before \n. StringBuilder offers a more readable style.

append(...): added a fragment

import kotlin.text.StringBuilder

fun main() {
    val sb = StringBuilder()

    sb.append("Sum: ")
    sb.append(10)
    sb.append(" euros")

    println(sb.toString()) // Sum: 10 euros
}

The neat part is that append can accept not only strings, but also numbers and characters. Kotlin/Java will convert it to text for you.

appendLine(...): added a line and a newline

appendLine() is a handy thing: it adds text and a newline. In the Kotlin standard library it’s an extension function for StringBuilder, and you can call it in a chain, like a “report builder.”

import kotlin.text.StringBuilder

fun main() {
    val sb = StringBuilder()
        .appendLine("Report")
        .appendLine("-----")
        .appendLine("Status: OK")

    print(sb.toString())
    // Report
    // -----
    // Status: OK
}

What’s especially nice here is that appendLine(...) returns the same StringBuilder, so you can write it as a chain.

Blank line: appendLine() with no arguments

Sometimes you need some “air” between blocks.

import kotlin.text.StringBuilder

fun main() {
    val sb = StringBuilder()
    sb.appendLine("First part")
    sb.appendLine() // blank line
    sb.appendLine("Second part")

    print(sb.toString())
    // First part
    //
    // Second part
}

4. Good practices for using StringBuilder

A very common beginner problem is to make a StringBuilder global, pass it through ten functions, and have everyone append “whatever they feel like.” You can do that, but it quickly turns into a textual version of something like: “in our project there’s one shared bundle of wires, don’t touch it, it’s held together by sheer willpower.”

A much better habit: inside a function, create a StringBuilder, assemble the text, and return a regular String.

Builder inside the function; outside — String

import kotlin.text.StringBuilder

fun buildHeader(title: String): String {
    val sb = StringBuilder()
    sb.appendLine("====")
    sb.appendLine(title)
    sb.appendLine("====")
    return sb.toString()
}

fun main() {
    print(buildHeader("My report"))
    // ====
    // My report
    // ====
}

There’s a very clear boundary here: the function returns a string, not a “semi-finished builder.” That’s convenient for testing, for reading code, and to avoid accidentally appending text “to the previous report.”

When you don’t need StringBuilder

After meeting StringBuilder, some people get tempted: “Oh, if it’s fast, I’ll build absolutely everything with it.” It’s like getting a power screwdriver as a gift and then using it to open yogurt. Technically possible, but weird.

If you’re building one short string, especially with string templates, then + or a template is totally fine.

fun main() {
    val name = "Ira"
    val tasks = 3

    println("User: $name, tasks: $tasks")
    // User: Ira, tasks: 3
}

A rule that usually works: if a string is built in a loop, if you have lots of appendLine(...), or you want to assemble multi-line text and return it from a function, StringBuilder is a great candidate. If the string is single and short, a template is simpler and clearer.

5. Practice: list report for a mini to-do list

By this point in the course, you already have enough tools to build a simple console “to-do list” app without complex data structures: a fixed array of strings + a counter for how many slots are actually used. We already know how to read a line, normalize it, parse a command via split(), use if/else, loops, and functions. Now we’ll add a nice output for the task list without turning the code into “100 lines of println.”

Let’s imagine the app state looks like this: there is a tasks array and a taskCount number that says how many tasks are actually filled.

Data stub

fun main() {
    val tasks = Array(5) { "" }
    var taskCount = 0

    tasks[taskCount++] = "Buy milk"
    tasks[taskCount++] = "Learn StringBuilder"
    tasks[taskCount++] = "Don’t concatenate strings in a loop like a barbarian"

    println("Total tasks: $taskCount") // Total tasks: 3
}

Bad output style: lots of println

You can do it this way, but it grows quickly:

fun main() {
    val tasks = arrayOf("A", "B", "C")

    println("Task list:")
    println("1. ${tasks[0]}")
    println("2. ${tasks[1]}")
    println("3. ${tasks[2]}")
}

As soon as you have not three tasks but “as many as the user enters,” you switch to a loop—and then you either have println inside the loop, or you assemble text. Printing inside a loop is fine, but sometimes we want to build a nice text first and then output it as one chunk (for example, to easily reuse this text elsewhere in the program).

Good style: assemble a report in StringBuilder

Let’s make a function that returns a ready-made multi-line report.

import kotlin.text.StringBuilder

fun buildTaskListReport(tasks: Array<String>, count: Int): String {
    val sb = StringBuilder()
    sb.appendLine("Task list:")

    for (i in 0 until count) {
        sb.appendLine("${i + 1}. ${tasks[i]}")
    }

    return sb.toString()
}

Now main can look neat:

fun main() {
    val tasks = Array(5) { "" }
    var taskCount = 0

    tasks[taskCount++] = "Buy milk"
    tasks[taskCount++] = "Learn StringBuilder"

    print(buildTaskListReport(tasks, taskCount))
    // Task list:
    // 1. Buy milk
    // 2. Learn StringBuilder
}

Notice print, not println: the report already contains line breaks, and an extra newline at the end isn’t always needed.

Aligned numbering

In the previous lecture you learned padStart()/padEnd(). We’ll use them so the numbers don’t “jump” if the task count becomes 12 or 123.

import kotlin.text.StringBuilder

fun buildTaskListReport(tasks: Array<String>, count: Int): String {
    val sb = StringBuilder()
    sb.appendLine("Task list:")

    for (i in 0 until count) {
        val num = (i + 1).toString().padStart(2)
        sb.appendLine("$num. ${tasks[i]}")
    }

    return sb.toString()
}

If there are 3 tasks, you’ll get:

1. ...
2. ...
3. ...

If there are 12 tasks:

9. ...
10. ...
11. ...
12. ...

And the text is visually more even, which means it’s easier to read.

6. Practice: help and error messages

When an app becomes at least a little interactive, the console “user interface” is not only a list of data, but also hints: how to enter commands, what an error means, what examples are allowed. Not the best approach is to store it as one huge string or as many println calls. But if the hint is even slightly dynamic (for example, depends on the task limit), StringBuilder becomes very convenient again.

Building help text with StringBuilder

import kotlin.text.StringBuilder

fun buildHelp(maxTasks: Int): String {
    val sb = StringBuilder()
    sb.appendLine("Commands:")
    sb.appendLine("  add <text>    — add a task")
    sb.appendLine("  list           — show the list")
    sb.appendLine("  exit           — quit")
    sb.appendLine()
    sb.appendLine("Task limit: $maxTasks")
    return sb.toString()
}

Usage:

fun main() {
    val maxTasks = 5
    print(buildHelp(maxTasks))
}

The benefit is that if you change the task limit, the help text will automatically be correct.

Unified message style: an error formatting function

import kotlin.text.StringBuilder

fun buildError(message: String): String {
    val sb = StringBuilder()
    sb.appendLine("Error:")
    sb.appendLine(message)
    return sb.toString()
}

fun main() {
    print(buildError("Unknown command. Type help."))
    // Error:
    // Unknown command. Type help.
}

Yes, you could have done "Error:\n$message\n". But as soon as the format becomes a bit more complex (a couple of lines, an example, a hint), the builder wins in readability.

7. Common mistakes when working with StringBuilder

Mistake #1: keep doing text += ... inside a loop “out of habit.”
It’s not a “sin,” the compiler won’t scold you, and the program will work. But with long texts you’ll get extra intermediate strings and less predictable performance. StringBuilder is designed exactly for the situation “I add small pieces many times.”

Mistake #2: forget toString() and try to print the builder as if it were a string.
In Kotlin, println(sb) will usually print the contents (because toString() gets called), and that’s sneaky: it feels like you can “just do that.” But as soon as you start returning this value from a function or passing it around as a String, you’ll find out the type is wrong. Good discipline: builder inside, only String outside.

Mistake #3: make one global StringBuilder for the entire application.
This quickly leads to “accidental text accumulation”: you built one report, then built a second, and it stuck to the first because nobody cleared the builder. It’s much safer to create a builder locally in the function where you format a specific text, and convert it to a String there.

Mistake #4: mix up append() and appendLine() and end up with “stuck together” output.
When a line break is important for readability, it’s better to use appendLine. When you’re building a single line from parts (for example, a table row), it’s better to use append. If you don’t distinguish these cases, everything will either merge into one line, or you’ll get weird blank lines.

Mistake #5: mix computation and formatting in one giant append(...).
Sometimes you want to write a monster like sb.appendLine("Total: ${a + b * c - ...}"). The compiler will swallow it, but a human is not required to. It’s more readable to compute first into a val total = ..., and then insert total into the string. You help yourself, and debugging becomes easier.

1
Task
Kotlin SELF,  9 level,  5 lecture
Locked
Robot greeting
Robot greeting
1
Task
Kotlin SELF,  9 level,  5 lecture
Locked
Log lines
Log lines
1
Task
Kotlin SELF,  9 level,  5 lecture
Locked
Statistics report
Statistics report
1
Task
Kotlin SELF,  9 level,  5 lecture
Locked
Item catalog
Item catalog
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet