StringBuilder и StringBuffer в Java
Склеить 10 000 строк через s += part в цикле — значит 10 000 раз создать новый объект String и каждый раз заново скопировать всё, что накоплено. Код работает, но с ростом данных замедляется квадратично. Именно для таких задач в Java есть StringBuilder.
StringBuilder в Java — это класс изменяемой последовательности символов из пакета java.lang: методы append(), insert(), delete(), replace() и reverse() меняют содержимое того же объекта, не создавая новую строку. StringBuffer — его потокобезопасный (синхронизированный) аналог. Пакет java.lang подключается автоматически, поэтому import для StringBuilder не нужен. Неизменяемый класс String и его методы разобраны в уроке Java: класс String и его методы.
Отличия String, StringBuilder и StringBuffer
String неизменяем: любая «модификация» создаёт новый объект. StringBuilder и StringBuffer изменяемы и меняют содержимое на месте. Разница между ними одна — методы StringBuffer синхронизированы (потокобезопасны), а StringBuilder — нет, поэтому в однопоточном коде StringBuilder быстрее и используется по умолчанию.
| Признак | String | StringBuilder | StringBuffer |
|---|---|---|---|
| Изменяемость | Неизменяемый (immutable) | Изменяемый | Изменяемый |
| Потокобезопасность | Да (за счёт неизменяемости) | Нет | Да, методы synchronized |
| Скорость многократных изменений | Низкая — каждый раз новый объект | Высокая | Чуть ниже StringBuilder из-за синхронизации |
| Появился | Java 1.0 | Java 5 | Java 1.0 |
equals() по содержимому | Да | Нет (сравнение ссылок) | Нет (сравнение ссылок) |
| Когда использовать | Значение не меняется, ключи HashMap, константы | Сборка строки в цикле, в одном потоке — почти всегда | Один объект реально меняют несколько потоков; старый API |
Все три класса объявлены как final и реализуют интерфейс CharSequence, поэтому многие методы JDK принимают любой из них.
Неочевидный момент
Синхронизация StringBuffer защищает только отдельный вызов метода. Последовательность if (sb.length() > 0) sb.append(", "); из двух потоков всё равно может дать гонку: между проверкой и добавлением другой поток успеет изменить буфер. Для составных операций нужна внешняя синхронизация, а чаще проще дать каждому потоку свой StringBuilder.
Когда использовать StringBuilder: конкатенация в цикле
StringBuilder нужен, когда строка собирается по частям в цикле или в нескольких местах кода. Оператор += в цикле на каждой итерации создаёт новый String и копирует в него весь накопленный текст, а StringBuilder дописывает символы в один и тот же буфер.
public class ConcatInLoop {
public static void main(String[] args) {
// Плохо: на каждой итерации создаётся новая строка
String s = "";
for (int i = 0; i < 5; i++) {
s += i;
}
System.out.println(s);
// Хорошо: один объект, данные дописываются в буфер
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
sb.append(i);
}
System.out.println(sb.toString());
}
}
// Вывод:
// 01234
// 01234 Результат одинаковый, но при 100 000 итерациях вариант с += копирует порядка n²/2 символов, а StringBuilder — линейное количество.
Для одиночного выражения вроде "Привет, " + name + "!" явный StringBuilder не нужен. До Java 8 включительно компилятор сам превращал такой + в цепочку StringBuilder.append(), а начиная с Java 9 (JEP 280) использует invokedynamic и StringConcatFactory, который работает не хуже ручного кода. Поэтому IntelliJ IDEA подсвечивает такой код инспекцией «StringBuilder can be replaced with String»: в одном выражении читаемый + лучше. Но в цикле оптимизация компилятора не помогает — там StringBuilder нужен явно. Соединить готовые строки через разделитель удобнее методом String.join().
Создание StringBuilder: конструкторы, length() и capacity()
StringBuilder создаётся через new одним из четырёх конструкторов. У объекта две разные «длины»: length() — сколько символов в строке сейчас, capacity() — размер внутреннего буфера, после заполнения которого буфер автоматически расширяется. Метода size() у StringBuilder нет.
| Конструктор | Содержимое | Начальная capacity() |
|---|---|---|
StringBuilder() | Пустая строка | 16 |
StringBuilder(int capacity) | Пустая строка | Заданное число; отрицательное — NegativeArraySizeException |
StringBuilder(String str) | Копия str | str.length() + 16; при null — NullPointerException |
StringBuilder(CharSequence seq) | Копия seq (например, другого StringBuilder) | seq.length() + 16 |
public class StringBuilderExample1 {
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder("Hello");
System.out.println("string = " + stringBuilder);
System.out.println("length = " + stringBuilder.length());
System.out.println("capacity = " + stringBuilder.capacity());
StringBuilder empty = new StringBuilder();
System.out.println("capacity = " + empty.capacity());
empty.append("12345678901234567"); // 17 символов не помещаются в 16
System.out.println("length = " + empty.length());
System.out.println("capacity = " + empty.capacity());
}
}
// Вывод:
// string = Hello
// length = 5
// capacity = 21
// capacity = 16
// length = 17
// capacity = 34 При переполнении буфер растёт примерно вдвое (старая capacity * 2 + 2, но не меньше требуемого), а старые данные копируются в новый массив. Если итоговый размер строки известен заранее, передайте его в конструктор new StringBuilder(1000) — это избавит от промежуточных копирований.
Что выведет этот код?
new StringBuilder('a').append("bc") выведет bc, а не abc. Конструктора с параметром char нет: символ 'a' расширяется до int 97 и вызывается StringBuilder(int capacity) — пустой объект с буфером на 97 символов. Чтобы начать с символа, пишите new StringBuilder("a") или new StringBuilder().append('a').
Методы StringBuilder: шпаргалка
Методы StringBuilder делятся на изменяющие объект (append, insert, delete, replace, reverse, setCharAt, setLength) и только читающие (charAt, indexOf, substring, length, toString). Изменяющие методы, кроме setCharAt и setLength, возвращают тот же StringBuilder, поэтому их можно вызывать цепочкой. У StringBuffer набор методов тот же.
| Метод | Что делает | Возвращает | Граничный случай |
|---|---|---|---|
append(x) | Добавляет в конец строковое представление любого значения | this | null-ссылка добавляется как текст "null" |
insert(offset, x) | Вставляет значение перед позицией offset | this | offset > length() — StringIndexOutOfBoundsException |
delete(start, end) | Удаляет символы с start по end - 1 | this | end > length() обрезается до длины, исключения нет |
deleteCharAt(index) | Удаляет один символ | this | Неверный индекс — StringIndexOutOfBoundsException |
replace(start, end, str) | Заменяет диапазон на строку любой длины | this | start > length() — исключение; end обрезается |
reverse() | Переворачивает строку | this | Суррогатные пары (эмодзи) не разрываются |
charAt(index) | Возвращает символ по индексу | char | Неверный индекс — StringIndexOutOfBoundsException |
setCharAt(index, ch) | Заменяет символ по индексу | void | Неверный индекс — StringIndexOutOfBoundsException |
setLength(n) | Обрезает строку или дополняет символами '\u0000' | void | setLength(0) очищает объект; отрицательное n — исключение |
length() / capacity() | Число символов / размер буфера | int | capacity() >= length() всегда |
indexOf(str), lastIndexOf(str) | Ищут подстроку | int | Не найдено — -1; принимают только String, не char |
substring(start, end) | Возвращает часть строки | String | Сам StringBuilder не меняется |
toString() | Создаёт новый String с текущим содержимым | String | Дальнейшие изменения builder на полученную строку не влияют |
compareTo(other) | Лексикографически сравнивает содержимое (Java 11+) | int | 0 — содержимое совпадает |
repeat(str, count) | Добавляет строку или символ count раз (Java 21+) | this | count < 0 — IllegalArgumentException |
Пример repeat() (Java 21): new StringBuilder().repeat("=", 10) даёт строку ========== — удобно для разделителей в консольном выводе.
Метод append()
Метод append() добавляет в конец StringBuilder строковое представление переданного значения: String, char, int, double, boolean, Object и т. д. Он возвращает тот же объект, поэтому вызовы удобно связывать в цепочку, а в конце получить строку через toString().
public class AppendExample {
public static void main(String[] args) {
int a = 42;
StringBuilder sb = new StringBuilder(40);
String s = sb.append("a = ").append(a).append('!').toString();
System.out.println(s);
String name = null;
StringBuilder greeting = new StringBuilder("Привет, ");
greeting.append(name);
System.out.println(greeting);
// greeting.append(null); // ошибка компиляции: reference to append is ambiguous
}
}
// Вывод:
// a = 42!
// Привет, null Переменная со значением null не вызывает исключения — добавляется текст "null". А литерал null напрямую не компилируется: подходят сразу несколько перегрузок (append(String), append(StringBuffer), append(char[])), и компилятор не может выбрать одну.
Метод insert()
Метод insert(offset, value) вставляет значение перед символом с индексом offset, сдвигая остаток строки вправо. Отдельного метода prepend() в Java нет: добавление в начало строки делается через insert(0, ...).
public class InsertExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("I Java!");
sb.insert(2, "like ");
System.out.println(sb);
sb.insert(0, ">> "); // добавление в начало
System.out.println(sb);
sb.insert(sb.length(), 21); // вставка в конец работает как append
System.out.println(sb);
sb.insert(100, "x"); // offset больше длины
}
}
// Вывод:
// I like Java!
// >> I like Java!
// >> I like Java!21
// Exception in thread "main" java.lang.StringIndexOutOfBoundsException: ... Допустимый offset — от 0 до length() включительно. Учтите, что вставка в начало длинной строки сдвигает все символы, поэтому многократный insert(0, ...) в цикле так же медленен, как +=; лучше добавлять через append() и один раз вызвать reverse() или собрать части в другом порядке.
Методы charAt(), setCharAt() и setLength()
Метод charAt(index) возвращает символ по индексу (нумерация с нуля), setCharAt(index, ch) заменяет его прямо в объекте, а setLength(n) обрезает строку до n символов. В отличие от String, где для замены одного символа приходится создавать новую строку, здесь изменение происходит на месте.
public class SetCharAtExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");
System.out.println(sb);
System.out.println("charAt(1) = " + sb.charAt(1));
sb.setCharAt(1, 'i');
System.out.println(sb);
sb.setLength(2);
System.out.println(sb);
sb.setLength(0); // очистка
System.out.println("length = " + sb.length());
}
}
// Вывод:
// Hello
// charAt(1) = e
// Hillo
// Hi
// length = 0 Если передать в setLength() число больше текущей длины, строка дополнится невидимыми символами '\u0000', а не пробелами — при выводе это выглядит как «пустота», но length() вырастет.
Методы delete() и deleteCharAt()
Метод delete(start, end) удаляет символы с индекса start включительно по end не включительно, deleteCharAt(index) удаляет один символ. Если end больше длины строки, он молча обрезается до length(), а вот неверный start приводит к StringIndexOutOfBoundsException.
public class DeleteExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("This is a test.");
sb.delete(4, 7);
System.out.println("После delete: " + sb);
sb.deleteCharAt(0);
System.out.println("После deleteCharAt: " + sb);
sb.delete(3, 100); // end больше длины — исключения нет
System.out.println("После delete(3, 100): " + sb);
}
}
// Вывод:
// После delete: This a test.
// После deleteCharAt: his a test.
// После delete(3, 100): his Удобный приём — убрать лишний разделитель после цикла: sb.setLength(sb.length() - 2) или sb.delete(sb.length() - 2, sb.length()) для хвоста ", ". Перед этим проверьте, что строка не пуста, иначе получите исключение.
Метод replace()
Метод replace(start, end, str) заменяет символы с start по end - 1 на строку str, причём её длина может отличаться от длины заменяемого фрагмента. Это замена по позиции, а не по содержимому: в отличие от String.replace(), StringBuilder не ищет подстроку сам.
public class ReplaceExample {
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder("Это простой тест.");
stringBuilder.replace(4, 11, "был");
System.out.println("После замены: " + stringBuilder);
}
}
// Вывод:
// После замены: Это был тест. Чтобы заменить фрагмент по тексту, сначала найдите его позицию через indexOf(), а затем вызовите replace(pos, pos + old.length(), newText).
Метод reverse()
Метод reverse() переворачивает последовательность символов и возвращает тот же объект. Это самый короткий способ развернуть строку в Java, но важно помнить: меняется сам StringBuilder, а не создаётся копия.
public class ReverseExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("abcdef");
String reversed = sb.reverse().toString();
System.out.println(reversed);
System.out.println(sb); // исходный объект тоже перевёрнут
}
}
// Вывод:
// fedcba
// fedcba Символы за пределами базовой плоскости Unicode (например, эмодзи) хранятся как суррогатные пары из двух char; reverse() обрабатывает такую пару как единое целое и не ломает символ.
Поиск и получение строки: indexOf(), substring(), toString()
Методы indexOf(), lastIndexOf() и substring() у StringBuilder работают так же, как у String, и не меняют объект. Метод toString() преобразует StringBuilder в String: создаёт новую неизменяемую строку с текущим содержимым.
public class SearchExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Java StringBuilder Java");
System.out.println(sb.indexOf("Java"));
System.out.println(sb.lastIndexOf("Java"));
System.out.println(sb.indexOf("Java", 1));
System.out.println(sb.substring(5, 18));
System.out.println(sb.length()); // substring не изменил объект
String result = sb.toString();
sb.append("!");
System.out.println(result); // строка не зависит от дальнейших изменений
}
}
// Вывод:
// 0
// 19
// 19
// StringBuilder
// 23
// Java StringBuilder Java У indexOf() в StringBuilder нет перегрузки для char: вызов sb.indexOf('J') не скомпилируется, нужно писать sb.indexOf("J"). Подробнее о поиске и извлечении подстрок в String — в уроке о классе String.
Сравнение StringBuilder: equals(), compareTo() и contentEquals()
Классы StringBuilder и StringBuffer не переопределяют методы equals() и hashCode() — используется реализация класса Object, то есть сравниваются ссылки. Два builder-а с одинаковым текстом не равны по equals().
public class StringBuilderEquals {
public static void main(String[] args) {
StringBuilder sb1 = new StringBuilder("Hello");
StringBuilder sb2 = new StringBuilder("Hello");
System.out.println(sb1.equals(sb2));
System.out.println(sb1 == sb2);
// Правильные способы сравнить содержимое:
System.out.println(sb1.toString().equals(sb2.toString()));
System.out.println(sb1.compareTo(sb2) == 0); // Java 11+
System.out.println("Hello".contentEquals(sb1)); // String vs StringBuilder
}
}
// Вывод:
// false
// false
// true
// true
// true Начиная с Java 11 StringBuilder и StringBuffer реализуют Comparable, поэтому их можно сравнивать через compareTo() и сортировать. Логика результата та же, что у String.compareTo(): отрицательное число, ноль или положительное. Из-за отсутствия equals()/hashCode() по содержимому StringBuilder нельзя использовать как ключ HashMap или элемент HashSet — кладите туда sb.toString().
На чём чаще всего ошибаются
Большинство ошибок с StringBuilder связано с тем, что его путают с String: ждут сравнения по содержимому, копии при изменении или не замечают перегрузку с int. Пять типичных промахов, которые встречаются и в коде, и на собеседованиях:
- Сравнение через
equals().sb1.equals(sb2)сравнивает ссылки и вернётfalseдаже для одинакового текста. ИспользуйтеcompareTo()илиtoString().equals()(см. раздел о сравнении). new StringBuilder('a'). Символ превращается вint, и создаётся пустой builder с capacity 97 (см. раздел о конструкторах).- Ожидание копии после
reverse(),append(),delete(). Эти методы меняют исходный объект. Если исходный текст нужен, сохранитеtoString()до изменения или создайте копиюnew StringBuilder(sb). - Создание StringBuilder внутри цикла.
for (...) { StringBuilder sb = new StringBuilder(); ... }теряет весь смысл: объект должен создаваться один раз до цикла. - StringBuilder ради одного выражения.
new StringBuilder().append(a).append(b).toString()вместоa + bтолько усложняет код: компилятор и так оптимизирует конкатенацию в одном выражении.
Часто задаваемые вопросы
Нужно ли импортировать StringBuilder в Java?
Нет. StringBuilder и StringBuffer находятся в пакете java.lang, который подключается к каждому файлу автоматически. Строка import java.lang.StringBuilder; допустима, но не нужна, и IDE пометит её как лишнюю.
Как преобразовать StringBuilder в String и обратно?
Из StringBuilder в String — sb.toString() или String.valueOf(sb). Из String в StringBuilder — конструктор new StringBuilder(str). Метод toString() каждый раз создаёт новую строку, поэтому в цикле вызывайте его один раз, после окончания сборки. Пример — в разделе indexOf(), substring() и toString().
Как очистить StringBuilder, чтобы использовать его повторно?
Вызовите sb.setLength(0): длина станет нулевой, а уже выделенный буфер сохранится, что экономит память при повторном заполнении. Вариант sb.delete(0, sb.length()) делает то же самое. Создать новый объект через new StringBuilder() тоже корректно, если старый больше нигде не используется.
StringBuffer устарел? Можно ли использовать StringBuilder в многопоточном приложении?
StringBuffer не помечен как @Deprecated, но в новом коде нужен редко. В многопоточном приложении StringBuilder безопасен, если объект живёт внутри одного метода или одного потока — это самый частый случай. StringBuffer нужен только тогда, когда один и тот же объект одновременно меняют несколько потоков.
Как узнать размер StringBuilder: size(), length() или capacity()?
Метода size() у StringBuilder нет. Количество символов возвращает length(), а capacity() показывает размер внутреннего буфера, который всегда не меньше длины. Лишний запас буфера можно освободить методом trimToSize().
Практика по теме
Закрепите урок на задачах — у каждой есть уровень и оценка времени.
Все задачи модуля →Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии