JavaRush /Курсы /Kotlin SELF /StringBuilder — сборка длинного текста

StringBuilder — сборка длинного текста

Kotlin SELF
9 уровень , 5 лекция
Открыта

1. Введение

Когда вы только начинаете программировать, конкатенация строк через + кажется идеальной: просто берём кусочки текста и склеиваем в один. И это действительно нормально, пока кусочков мало. Но как только у нас появляется цикл, много строк и «сборка отчёта», внезапно оказывается, что склеивание в лоб создаёт слишком много промежуточных строк — как будто вы каждый раз переписываете весь отчёт заново, добавляя одну букву.

String неизменяемая

Строка (String) в Kotlin неизменяема. Это значит: если вы написали val s2 = s1 + "!", то s1 не «расширилась», а создалась новая строка s2. Старую строку изменить нельзя — «что написано пером», так сказать.

Это удобно и безопасно, но иногда дорого.

Плохой паттерн: += в цикле

Посмотрите на пример. Он рабочий, но с подвохом:

fun main() {
    var text = ""

    for (i in 1..3) {
        text += "Строка №$i\n"
    }

    print(text)
    // Строка №1
    // Строка №2
    // Строка №3
}

Проблема тут не в том, что код «неправильный». Проблема в том, что на каждой итерации создаётся новая строка text, в которую копируется всё старое содержимое + новый кусок. На трёх строках вы этого не заметите. На трёх тысячах — ваш компьютер начнёт немного задумываться о смысле жизни.

Что мы хотим вместо этого

Нам нужен инструмент, который позволит «дописывать текст внутрь одного объекта», не создавая заново гигантскую строку на каждом шаге. И вот тут на сцену выходит StringBuilder.

2. StringBuilder: что это и как он работает

Если String — это «надпись на камне», то StringBuilder — это «блокнот с карандашом». В блокнот можно дописывать строки по одной, стирать, перестраивать (в рамках возможностей), и только в конце сказать: «Так, теперь аккуратно перепиши это набело в красивую строку».

StringBuilder — это специальный класс для поэтапной сборки текста. Он хранит внутренний буфер символов и умеет эффективно дополнять его.

Три основных этапа

Полезно держать простую картинку:

flowchart LR
    A["Кусочки текста"] --> B["StringBuilder (копим)"]
    B --> C["toString() (получили готовую String)"]

То есть логика такая: «копим» → «превращаем в строку» → «печатаем/возвращаем».

Минимальный пример: собрать фразу

import kotlin.text.StringBuilder

fun main() {
    val sb = StringBuilder()
    sb.append("Привет")
    sb.append(", ")
    sb.append("мир!")

    println(sb.toString()) // Привет, мир!
}

Обратите внимание: мы не делаем String + String + String. Мы дописываем в один объект sb, а в конце вызываем toString().

String и StringBuilder простыми словами

String — это готовая неизменяемая строка: она отлично подходит для коротких значений, шаблонов вроде "Привет, $name" и ситуаций, где вы один раз собрали строку и дальше её только читаете.

StringBuilder — это изменяемый «сборщик»: он удобен, когда вы собираете длинный текст по кусочкам, особенно в цикле, и хотите в конце получить итоговую String через toString().

3. append() и appendLine() для сборки текста

Когда вы начинаете писать отчёты, вы довольно быстро понимаете, что «дописать кусок текста» и «дописать строку целиком + перевод строки» — это два разных действия. Можно, конечно, всегда добавлять "\n" руками, но тогда вы быстро начнёте путаться: где забыли перенос, где сделали два переноса подряд, где случайно прилепили пробел перед \n. StringBuilder предлагает более читабельный стиль.

append(...): добавили кусочек

import kotlin.text.StringBuilder

fun main() {
    val sb = StringBuilder()

    sb.append("Сумма: ")
    sb.append(10)
    sb.append(" евро")

    println(sb.toString()) // Сумма: 10 евро
}

Фишка тут в том, что append умеет принимать не только строки, но и числа, и символы. Kotlin/Java сами превратят это в текст.

appendLine(...): добавили строку и перенос

appendLine() — удобная штука: она добавляет текст и перевод строки. В стандартной библиотеке Kotlin это extension-функция для StringBuilder, и её можно вызывать цепочкой, как «конструктор отчёта».

import kotlin.text.StringBuilder

fun main() {
    val sb = StringBuilder()
        .appendLine("Отчёт")
        .appendLine("-----")
        .appendLine("Статус: OK")

    print(sb.toString())
    // Отчёт
    // -----
    // Статус: OK
}

Здесь особенно приятно то, что appendLine(...) возвращает тот же StringBuilder, поэтому можно писать цепочкой.

Пустая строка: appendLine() без аргументов

Иногда нужен «воздух» между блоками.

import kotlin.text.StringBuilder

fun main() {
    val sb = StringBuilder()
    sb.appendLine("Первая часть")
    sb.appendLine() // пустая строка
    sb.appendLine("Вторая часть")

    print(sb.toString())
    // Первая часть
    //
    // Вторая часть
}

4. Хорошие практики использования StringBuilder

Очень частая проблема начинающих — сделать StringBuilder глобальным, передавать его по десяти функциям и дописывать «кто во что горазд». Так тоже можно, но это быстро превращается в текстовую версию чего-то типа «у нас в проекте один общий пакет с проводами, трогать нельзя, он держится на честном слове».

Гораздо лучше привычка: внутри функции мы создаём StringBuilder, собираем текст и возвращаем обычную String.

Builder внутри функции, наружу — 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("Мой отчёт"))
    // ====
    // Мой отчёт
    // ====
}

Здесь очень чёткая граница: функция возвращает строку, а не «полуфабрикат для сборки». Это удобно для тестирования, для чтения кода и для того, чтобы случайно не дописать текст «в прошлый отчёт».

Когда StringBuilder не нужен

После знакомства со StringBuilder у некоторых появляется соблазн: «О, раз он быстрый, буду собирать вообще всё через него». Это похоже на ситуацию, когда вам подарили шуруповёрт, и вы начинаете им открывать йогурт. Технически возможно, но странно.

Если вы строите одну короткую строку, особенно через string templates, то + или шаблон — абсолютно ок.

fun main() {
    val name = "Ира"
    val tasks = 3

    println("Пользователь: $name, задач: $tasks")
    // Пользователь: Ира, задач: 3
}

Правило, которое обычно работает: если строка строится в цикле, если у вас много appendLine(...), или вы хотите собрать многострочный текст и вернуть его из функции — StringBuilder отличный кандидат. Если строка одна и короткая — шаблон проще и понятнее.

5. Практика: отчёт list для мини‑списка задач

К этому моменту курса у вас уже достаточно инструментов, чтобы собрать простое консольное приложение «список задач» без сложных структур данных: фиксированный массив строк + счётчик количества занятых элементов. Мы уже умеем читать строку, нормализовывать, разбирать команду через split(), использовать if/else, циклы и функции. Сейчас добавим красивый вывод списка задач, не превращая код в «100 строк из println».

Представим, что состояние приложения такое: есть массив tasks и число taskCount, которое говорит, сколько задач реально заполнено.

Заготовка данных

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

    tasks[taskCount++] = "Купить молоко"
    tasks[taskCount++] = "Выучить StringBuilder"
    tasks[taskCount++] = "Не склеивать строки в цикле как варвар"

    println("Всего задач: $taskCount") // Всего задач: 3
}

Плохой стиль вывода: много println

Так делать можно, но это быстро разрастается:

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

    println("Список задач:")
    println("1. ${tasks[0]}")
    println("2. ${tasks[1]}")
    println("3. ${tasks[2]}")
}

Как только у вас не три задачи, а «сколько введёт пользователь», вы переходите на цикл — и тут появляется либо println внутри цикла, либо сборка текста. Печать внутри цикла нормальна, но иногда нам хочется сначала построить красивый текст, а потом вывести одним куском (например, чтобы легко переиспользовать этот текст в других местах программы).

Хороший стиль: собрать отчёт в StringBuilder

Сделаем функцию, которая возвращает готовый многострочный отчёт.

import kotlin.text.StringBuilder

fun buildTaskListReport(tasks: Array<String>, count: Int): String {
    val sb = StringBuilder()
    sb.appendLine("Список задач:")

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

    return sb.toString()
}

Теперь main может выглядеть аккуратно:

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

    tasks[taskCount++] = "Купить молоко"
    tasks[taskCount++] = "Выучить StringBuilder"

    print(buildTaskListReport(tasks, taskCount))
    // Список задач:
    // 1. Купить молоко
    // 2. Выучить StringBuilder
}

Обратите внимание на print, а не println: отчёт уже содержит переносы строк, и лишний перевод строки в конце не всегда нужен.

Нумерация с выравниванием

В прошлой лекции вы изучали padStart()/padEnd(). Используем их, чтобы номера не «плясали», если задач станет 12 или 123.

import kotlin.text.StringBuilder

fun buildTaskListReport(tasks: Array<String>, count: Int): String {
    val sb = StringBuilder()
    sb.appendLine("Список задач:")

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

    return sb.toString()
}

Если задач 3, получится:

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

Если задач 12, то:

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

И текст визуально ровнее, а значит — читается легче.

6. Практика: help и сообщения об ошибках

Когда приложение становится хоть немного интерактивным, «пользовательский интерфейс» в консоли — это не только список данных, но и подсказки: как вводить команды, что означает ошибка, какие примеры допустимы. Не самый лучший способ — хранить это как огромную строку или много println. Но если подсказка хоть чуть-чуть динамическая (например, зависит от лимита задач), StringBuilder снова оказывается очень удобным.

Сборка help-текста через StringBuilder

import kotlin.text.StringBuilder

fun buildHelp(maxTasks: Int): String {
    val sb = StringBuilder()
    sb.appendLine("Команды:")
    sb.appendLine("  add <текст>    — добавить задачу")
    sb.appendLine("  list           — показать список")
    sb.appendLine("  exit           — выйти")
    sb.appendLine()
    sb.appendLine("Лимит задач: $maxTasks")
    return sb.toString()
}

Использование:

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

Плюс тут в том, что если вы поменяете лимит задач — help автоматически будет корректным.

Единый стиль сообщений: функция форматирования ошибки

import kotlin.text.StringBuilder

fun buildError(message: String): String {
    val sb = StringBuilder()
    sb.appendLine("Ошибка:")
    sb.appendLine(message)
    return sb.toString()
}

fun main() {
    print(buildError("Неизвестная команда. Напишите help."))
    // Ошибка:
    // Неизвестная команда. Напишите help.
}

Да, можно было сделать "Ошибка:\n$message\n". Но как только формат станет чуть сложнее (пара строк, пример, подсказка) — builder выигрывает по читаемости.

7. Типичные ошибки при работе со StringBuilder

Ошибка №1: продолжать делать text += ... внутри цикла «по привычке».
Это не «грех», компилятор вас не отругает, и программа будет работать. Но на длинных текстах вы получите лишние промежуточные строки и менее предсказуемую производительность. StringBuilder как раз создан для ситуации «добавляю по кусочку много раз».

Ошибка №2: забывать toString() и пытаться печатать builder как будто это строка.
В Kotlin println(sb) обычно выведет содержимое (потому что вызовется toString()), и это коварно: кажется, что можно «и так». Но как только вы начнёте возвращать это значение из функции или передавать дальше как String, выяснится, что тип не тот. Хорошая дисциплина: builder внутри, наружу — только String.

Ошибка №3: делать один глобальный StringBuilder на всё приложение.
Такое решение быстро приводит к «случайному накоплению текста»: вы построили отчёт, потом построили второй, а он прилип к первому, потому что builder никто не очистил. Гораздо безопаснее создавать builder локально в функции, где вы формируете конкретный текст, и там же превращать его в String.

Ошибка №4: путать append() и appendLine() и получать «слипшийся» вывод.
Когда перенос строки важен для читаемости, лучше писать appendLine. Когда вы строите одну строку из частей (например, строку таблицы), лучше append. Если не различать эти случаи, у вас либо всё сольётся в одну строку, либо появятся странные пустые строки.

Ошибка №5: смешивать вычисления и форматирование в одном гигантском append(...).
Иногда хочется написать монстра вида sb.appendLine("Итого: ${a + b * c - ...}"). Компилятор это проглотит, но человек — не обязан. Читаемее сначала посчитать в val total = ..., а потом подставить total в строку. Вы и себе помогаете, и отладка станет легче.

1
Задача
Kotlin SELF, 9 уровень, 5 лекция
Недоступна
Приветствие робота
Приветствие робота
1
Задача
Kotlin SELF, 9 уровень, 5 лекция
Недоступна
Лог строк
Лог строк
1
Задача
Kotlin SELF, 9 уровень, 5 лекция
Недоступна
Отчёт статистики
Отчёт статистики
1
Задача
Kotlin SELF, 9 уровень, 5 лекция
Недоступна
Каталог предметов
Каталог предметов
1
Опрос
Продвинутая работа со строками, 9 уровень, 5 лекция
Недоступен
Продвинутая работа со строками
Продвинутая работа со строками
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
kasnil Уровень 54
18 марта 2026
В опросе Продвинутая работа со строками, 5 вопрос: Как корректно вывести текст Цена: $19, если val price = 19, нет правильного ответа из предложенных. Вариант принимаемого как верный: println("Цена: \$${'$'}price") не корректен.