Коли використовувати StringBuilder vs 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. При відомих розмірах завжди задавайте 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. Що відбувається при конкатенації рядків через оператор +]]