W3docs

Java StringBuilder

Эффективное создание изменяемых строк в Java с помощью класса StringBuilder — append, insert, reverse и многое другое.

String неизменяем; наращивание строки через += в цикле работает за квадратичное время. StringBuilder — это ответ стандартной библиотеки: единственный объект с изменяемым внутренним буфером, к которому можно добавлять, вставлять, удалять и переворачивать данные на месте, а затем единожды преобразовать его в неизменяемый String. Это основной инструмент для всех современных шаблонов построения строк в JDK, и именно к нему стоит обращаться, как только вы начинаете накапливать текст в цикле или через несколько вызовов методов.

Создание билдера

StringBuilder sb = new StringBuilder();             // empty, capacity 16
StringBuilder withCap = new StringBuilder(1024);    // empty, preallocated capacity
StringBuilder fromText = new StringBuilder("hi ");  // capacity = length + 16

Конструктор без аргументов начинает с ёмкостью 16, что подходит для коротких результатов, но является пессимистичным выбором для длинных. Если вы примерно знаете, какого размера будет итоговая строка, передайте ёмкость заранее — каждое предотвращённое расширение означает одно меньше выделений массива и одно меньше arraycopy. new StringBuilder(estimatedLength) — наиболее эффективная микрооптимизация во всём этом разделе книги.

Цепочки вызовов: каждый мутирующий метод возвращает this

Каждый мутирующий метод StringBuilder возвращает сам билдер, поэтому вызовы можно объединять в одно выражение:

String greeting = new StringBuilder()
    .append("Hello, ")
    .append(name)
    .append('!')
    .append('\n')
    .toString();

Это соглашение, а не магия; можно разбить на отдельные операторы без какой-либо функциональной разницы. Стиль с цепочками отражает то, как компилятор переписывает цепочки + под капотом, что само по себе объясняет, почему такой стиль естественно читается в Java.

Семейство мутирующих методов

У StringBuilder небольшой, сфокусированный интерфейс:

  • append — добавляет текст или любой примитив в конец. Перегружен для каждого примитива, char[], CharSequence и Object (вызывает toString).
  • insert(offset, ...) — те же перегрузки, но в произвольную позицию.
  • delete(start, end), deleteCharAt(i) — удаляет диапазон или одиночный символ.
  • replace(start, end, replacement) — заменяет диапазон новой подстрокой; длины могут отличаться.
  • reverse() — переворачивает буфер на месте.
  • setCharAt(i, ch) — перезаписывает одиночный символ.
  • setLength(n) — усекает (или дополняет пробелами ) до n символов.

Эти методы редактируют буфер; они не возвращают новый String. Чтобы получить снимок содержимого в виде неизменяемой строки, вызовите toString().

Инспекция и конвертация

  • length() — текущее количество символов.
  • capacity() — текущий размер внутреннего массива. Всегда ≥ length().
  • charAt(i), substring(start) / substring(start, end) — доступ на чтение, идентично String.
  • indexOf(s), lastIndexOf(s) — поиск подстроки.
  • toString() — создаёт неизменяемый String. Вызывайте один раз в конце; многократный вызов в процессе построения — бесполезные выделения памяти.
  • ensureCapacity(n) — предварительно расширяет буфер как минимум до n.
  • trimToSize() — сжимает буфер до текущего содержимого. Требуется редко.

Как растёт буфер

Внутри StringBuilder хранит byte[] (или char[] в старых JDK). Когда append приводит к переполнению, буфер перераспределяется примерно до 2 × oldCapacity + 2, и старое содержимое копируется. Каждое расширение — O(n) от текущего размера, но паттерн удвоения делает суммарную стоимость n добавлений O(n) амортизированно — совершенно иначе, чем повторная конкатенация String, где тот же цикл даёт O(n²).

append "a" — capacity 16, length 1
... 15 more — capacity 16, length 16
append "b" — grow to 34, length 17
... 17 more — capacity 34, length 34
append "c" — grow to 70, length 35

Если вы знаете итоговую длину, можно пропустить все эти перераспределения, задав нужную ёмкость при создании.

StringBuilder vs оператор +

Для коротких, статически известных сборок строк компилятор сам делает всё правильно. "Hello, " + name + "!" переписывается на этапе компиляции либо в единственную цепочку StringBuilder, либо в вызов StringConcatFactory.makeConcatWithConstants (Java 9+). Оба варианта эффективны. Нет нужды вручную управлять такими выражениями.

Следует избегать += внутри цикла с неизвестным числом итераций:

// O(n²) — every += allocates a new String holding everything seen so far
String out = "";
for (String token : tokens) {
  out += token + "|";
}

// O(n) — one buffer, one final String
StringBuilder sb = new StringBuilder();
for (String token : tokens) {
  sb.append(token).append('|');
}
String out = sb.toString();

Если цикл выполняется несколько раз, разница незаметна. При нескольких тысячах токенов это уже измеримая проблема. При миллионе — первый вариант зависнет, а второй завершится за миллисекунды.

StringBuilder не является потокобезопасным

StringBuilder намеренно не использует синхронизацию, чтобы быть быстрым в подавляющем большинстве однопоточных случаев. Если два потока одновременно добавляют данные в один билдер, результаты непредсказуемы: потерянные записи, перезаписанные символы или ArrayIndexOutOfBoundsException на пути расширения. В редком случае, когда один билдер разделяется между потоками, используйте синхронизированный аналог — StringBuffer. На практике билдер почти никогда не разделяется; каждый поток строит свою собственную строку.

Пример с разбором

Программа ниже демонстрирует все важные части интерфейса для повседневного кода: цепочки append, явный insert, replace с изменением длины диапазона, reverse и единственный финальный toString. Ёмкость выводится в начале и в конце, чтобы сделать видимым удвоение буфера.

java— editable, runs on the server

Обратите внимание на числа ёмкости в выводе (capacity=32 в начале, capacity=66 в конце). Все добавления в цепочке помещаются в начальную ёмкость 32 — построенная строка имеет всего 24 символа. Затем insert и replace увеличивают длину до 40, что превышает 32 и вызывает ровно одно расширение (до 2 × 32 + 2 = 66). Если задать ёмкость ближе к реальной итоговой длине — new StringBuilder(48) здесь — это единственное перераспределение было бы полностью исключено. Вот и вся суть: чем точнее оценка ёмкости, тем меньше событий копирования при росте.

Что дальше

StringBuilder быстр именно потому, что не является потокобезопасным. Его синхронизированный аналог — правильный выбор в редком случае, когда вам действительно нужен один буфер, разделяемый между потоками — тот же API, методы с блокировкой. Продолжайте с Java StringBuffer.

Практика

Практика
Почему `StringBuilder` обычно предпочтительнее конкатенации `String` в цикле?
Почему `StringBuilder` обычно предпочтительнее конкатенации `String` в цикле?
Was this page helpful?