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. Ёмкость выводится в начале и в конце, чтобы сделать видимым удвоение буфера.
Обратите внимание на числа ёмкости в выводе (capacity=32 в начале, capacity=66 в конце). Все добавления в цепочке помещаются в начальную ёмкость 32 — построенная строка имеет всего 24 символа. Затем insert и replace увеличивают длину до 40, что превышает 32 и вызывает ровно одно расширение (до 2 × 32 + 2 = 66). Если задать ёмкость ближе к реальной итоговой длине — new StringBuilder(48) здесь — это единственное перераспределение было бы полностью исключено. Вот и вся суть: чем точнее оценка ёмкости, тем меньше событий копирования при росте.
Что дальше
StringBuilder быстр именно потому, что не является потокобезопасным. Его синхронизированный аналог — правильный выбор в редком случае, когда вам действительно нужен один буфер, разделяемый между потоками — тот же API, методы с блокировкой. Продолжайте с Java StringBuffer.