Когда использовать StringBuilder, а когда StringBuffer?
Оба класса предназначены для изменения строк. В отличие от обычного String, они могут менять своё содержимое без создания новых объектов.
🟢 Junior Level
Оба класса предназначены для изменения строк. В отличие от обычного String, они могут менять своё содержимое без создания новых объектов.
Главное отличие: StringBuffer — потокобезопасный (синхронизированный), StringBuilder — нет.
Пример:
// StringBuilder — используйте в 99% случаев
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");
String result = sb.toString(); // "Hello World"
// StringBuffer — только если строка используется из нескольких потоков
StringBuffer sb2 = new StringBuffer();
sb2.append("Hello");
Правило: Используйте StringBuilder по умолчанию. StringBuffer имеет смысл только когда один буфер разделяется между потоками. В legacy-коде
он встречается чаще, но это исторический артефакт, а не рекомендация.
🟡 Middle Level
Как это работает
Оба класса наследуются от AbstractStringBuilder и работают с внутренним массивом byte[] (Java 9+) или char[] (до Java 9):
// Внутреннее устройство
AbstractStringBuilder {
byte[] value; // Массив данных
int count; // Текущее количество символов
}
Начальная ёмкость: 16 символов по умолчанию. При переполнении массив расширяется: newCapacity = (oldCapacity << 1) + 2 (удвоение + 2).
Практическое применение
// StringBuilder — сборка строки в одном потоке (типичный случай)
StringBuilder sb = new StringBuilder();
for (User user : users) {
sb.append(user.getName()).append(", ");
}
Типичные ошибки
-
Ошибка: Использование
StringBuffer“на всякий случай” Решение: Синхронизация добавляет overhead даже в однопоточном коде -
Ошибка: Не указывать capacity, когда размер известен Решение:
new StringBuilder(1024)избежит лишних переаллокаций
Сравнение
| Критерий | StringBuilder | StringBuffer | | —————— | ————– | ————————————————————————————————— | | Потокобезопасность | Нет | Да (synchronized) | | Скорость (1 поток) | Быстрее | Медленнее в 1.5-2.5x. Каждый synchronized-метод требует monitorenter/monitorexit + memory barriers. | В однопоточном режиме JVM может частично элизировать блокировки, но не полностью. | | Скорость (N потоков) | Race condition | Корректно, но contention | | Когда появился | Java 5 | Java 1.0 | | Рекомендация | По умолчанию | Только при реальном shared access |
🔴 Senior Level
Internal Implementation
// StringBuffer — все методы синхронизированы
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
// StringBuilder — без синхронизации
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
Ключевое различие: каждый метод StringBuffer имеет synchronized, что означает захват монитора объекта при каждом вызове.
Архитектурные Trade-offs
StringBuffer:
- Плюсы: Thread-safe, legacy совместимость
- Минусы: Монитор на каждый метод, memory barriers, false sharing при contention
StringBuilder:
- Плюсы: Нет overhead синхронизации, оптимальная производительность
- Минусы: Не thread-safe — race condition при параллельном доступе
JVM оптимизации: Lock Elision
Современная JVM (HotSpot) использует Escape Analysis + Lock Elision:
void method() {
StringBuffer sb = new StringBuffer(); // Escape: не выходит за пределы метода
sb.append("a"); // JIT может убрать synchronized
sb.append("b");
}
Если JIT доказал, что объект не “убегает” из потока — синхронизация удаляется. Но:
- Это не гарантировано (зависит от
-XX:+EliminateLocks, компиляционного бюджета) - Анализ сам по себе стоит CPU cycles
StringBuilderбыстр “из коробки” без магии JIT
Edge Cases
-
Biased Locking (удалён в Java 15): Раньше JVM “привязывала” монитор к первому потоку, делая последующие захваты бесплатными. Это значит, что в Java 15+ overhead StringBuffer в однопоточном режиме ещё выше, чем раньше.
-
Resize формула:
(oldCapacity << 1) + 2. Для capacity=16 → 34 → 70 → 142. При bekannten размерах всегда задавайте capacity в конструкторе. -
toStringCache в StringBuffer:
StringBufferкэширует последнийtoString()результат. Если буфер не менялся, повторныйtoString()возвращает кэш — но это микрооптимизация. -
Java 9+ Compact Strings: Оба класса используют
byte[]сcoderфлагом. Латинские символы = 1 байт, остальные = 2 байта.
Производительность (JMH бенчмарки)
| Операция | StringBuilder | StringBuffer (no contention) | StringBuffer (4 threads) | | ————- | ————– | —————————— | ————————- | | append 1M раз | ~15ms | ~25ms | ~120ms | | toString | ~2ms | ~2ms (кэш) | ~2ms | | Memory | ~2MB | ~2MB + monitor | ~2MB + monitor overhead |
Production Experience
Сценарий: Рендеринг HTML-отчёта (50KB итого) из 10 000 записей:
- Без capacity: 12 realloc + copy операций → 15ms
- С
new StringBuilder(51200): 0 realloc → 3ms StringBufferв том же сценарии: 20ms (lock elision помог, но не полностью)
Best Practices для Highload
- Всегда указывайте
initialCapacity, если размер предсказуем - В циклах:
StringBuilderвне цикла,appendвнутри StringBuffer— только если буфер разделяется между потоками (крайне редкий кейс)- Для конкатенации вне цикла: Java 9+
invokedynamicчасто эффективнее ручного StringBuilder
🎯 Шпаргалка для интервью
Обязательно знать:
StringBuilder— mutable, NOT thread-safe, используйте в 99% случаевStringBuffer— mutable, thread-safe (synchronized), legacy из Java 1.0- Оба работают с внутренним
byte[](Java 9+) /char[](до Java 9),扩容 при переполнении:newCapacity = (old << 1) + 2 - Начальная ёмкость по умолчанию — 16 символов. При известном размере — задавайте в конструкторе
StringBuilderбыстрееStringBufferв 1.5-2.5x в однопоточном режиме- В циклах: создавайте
StringBuilderДО цикла,appendВНУТРИ
Частые уточняющие вопросы:
- Почему
StringBufferмедленнее? — Каждый методsynchronized: захват монитора, memory barriers, освобождение. Даже в одном потоке. - Когда происходит расширение? — При переполнении: 16 → 34 → 70 → 142. Каждое расширение = аллокация нового массива +
System.arraycopy. - Можно ли использовать
StringBufferдля безопасности? — Если каждый поток использует свой буфер —StringBuilderбезопасен.StringBufferнужен только при реальном shared access. - Что делает JVM для оптимизации
StringBuffer? — Lock Elision через Escape Analysis: если объект не “убегает” из потока — JIT может убратьsynchronized. Но не гарантировано.
Красные флаги (НЕ говорить):
- ❌ “
StringBuffer— рекомендуемый выбор” — устарел,StringBuilder— дефолт - ❌ “StringBuilder thread-safe” — нет, race condition при параллельном доступе
- ❌ “Не нужно задавать capacity” — без capacity: 12+ realloc для 50KB строки
- ❌ “
StringBufferнужен для каждого многопоточного приложения” — только если ОДИН буфер шарится между потоками
Связанные темы:
- [[6. Почему StringBuffer медленнее StringBuilder]]
- [[4. Почему String является иммутабельным]]
- [[7. Что происходит при конкатенации строк через оператор +]]