Що відбувається при конкатенації рядків через оператор +?
Коли ви використовуєте + для склеювання рядків, Java створює новий рядок з двох (або більше) початкових. Оскільки String незмінний, оригінальні рядки не змінюються.
🟢 Junior Level
Коли ви використовуєте + для склеювання рядків, Java створює новий рядок з двох (або більше) початкових. Оскільки String незмінний, оригінальні рядки не змінюються.
Приклад:
String s1 = "Hello";
String s2 = "World";
String result = s1 + " " + s2; // "Hello World"
Важливий момент: Якщо складати рядки в циклі через +, на кожній ітерації створюється новий рядок. Це дуже неефективно.
// ПОГАНО — створює N нових об'єктів String
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // new String на кожній ітерації!
}
// ГАРНО — один об'єкт StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
Правило: + зручний для простих виразів (1-2 рядки), але заборонений у циклах.
Коли + шкодить читабельності
Складні вирази з вкладеними викликами методів:
"User: " + user.getName() + " (" + user.getAge() + " years)" — краще StringBuilder або форматування.
🟡 Middle Level
Як це працює
Поведінка залежить від того, чи відомі значення на етапі компіляції:
Константна конкатенація (Compile-time Constant Folding):
String s = "Hello" + " " + "World";
// Компілятор перетворює це на:
String s = "Hello World"; // Один рядок у байт-коді, 0 операцій у рантаймі
Конкатенація змінних (Runtime):
String a = getName();
String b = getLastName();
String full = a + " " + b;
Що відбувається “під капотом” залежить від версії Java (див. Senior секцію).
Типові помилки
-
Помилка:
+у циклі Рішення:StringBuilderпоза циклом -
Помилка:
split(".")— крапка це regex-метасимвол Рішення:split("\\.")абоsplit(Pattern.quote(".")) -
Помилка:
null+ рядок Рішення: Результат буде"nullWorld". Це не помилка, але може здивувати.
Порівняння підходів
| Сценарій | Підхід | Ефективність |
| ———————————- | ———————————– | ——————————— |
| "A" + "B" + "C" | + | Відмінно (constant folding) |
| a + b + c (змінні, 1 рядок) | + | Відмінно (оптимізується) |
| Цикл з += | StringBuilder | + — O(n²), StringBuilder — O(n) |
| Форматування | String.format() або StringBuilder| Залежить від контексту |
🔴 Senior Level
Internal Implementation — Еволюція
Java 5-8: StringBuilder codegen
Компілятор замінював a + b + c на:
new StringBuilder().append(a).append(b).append(c).toString()
Проблема: байт-код “зашитий” у .class. Нова стратегія оптимізації потребувала б перекомпіляції.
JEP 280 (Java 9) ввів invokedynamic для конкатенації. За замовчуванням у Java 9 використовується BC_SB, у Java 11+ стратегія може змінюватись через StringConcatFactory.
String full = a + " " + b;
Байт-код:
0: aload_1 // a
1: aload_2 // " "
2: aload_3 // b
3: invokedynamic #7 // makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
8: astore 4
invokedynamic посилається на StringConcatFactory. При першому виклику bootstrap-метод генерує оптимальний код для даної конкретної конкатенації.
Стратегії StringConcatFactory:
- MH_LF (MethodHandle with LambdaForm): Генерує MethodHandle для конкатенації
- BC_SB (Bytecode StringBuilder): Генерує байт-код із StringBuilder
- BH_SB (Bootstrap method with StringBuilder — compact): Оптимізований варіант
Архітектурні Trade-offs
Плюси invokedynamic:
- JVM обирає стратегію у рантаймі (adaptivity)
- Можна додавати нові стратегії без перекомпіляції
- Менше алокацій — JVM знає всі аргументи і виділяє
byte[]потрібного розміру одразу
Мінуси:
- Bootstrap overhead при першому виклику (~1-5μs)
- Складніше дебажити байт-код
Edge Cases
-
null конкатенація:
"val: " + null→"val: null".StringConcatFactoryіStringBuilderвикликаютьString.valueOf(null)→"null". -
Змішування типів:
"Value: " + 42→StringConcatFactoryзнає типintі використовує оптимальну конвертацію без створення проміжногоString. -
Цикли:
invokedynamicоптимізує лише один вираз. Цикл як і раніше створює новий контекст конкатенації на кожній ітерації:for (int i = 0; i < n; i++) s += i; // Кожна ітерація: invokedynamic → новий byte[] → новий String // Total: O(n²) алокацій
Продуктивність
| Сценарій | Java 8 (StringBuilder) | Java 9+ (invokedynamic) | | —————— | ———————- | ————————– | | 3 змінних | ~30ns | ~15ns | | 10 змінних | ~80ns | ~30ns | | 3 змінних + int | ~35ns | ~18ns | | Цикл 10K ітерацій | O(n²) | O(n²) (не оптимізується!) |
Production Experience
Сценарій: Логування log.info("User " + user.getId() + " action " + action) — 100K викликів/сек:
- Java 8: 100K
new StringBuilder()→ ~30ms CPU - Java 9+: invokedynamic з прямою алокацією → ~15ms CPU
- Якщо лог відфільтровано (level < INFO): обидва варіанти витрачають CPU даремно. Рішення:
if (log.isInfoEnabled())
Best Practices для Highload
- Для простих конкатенацій (1 рядок коду):
+— читабельно і ефективно - У циклах: завжди
StringBuilderзinitialCapacity - Для форматування:
String.format()зручний, але повільніший (парсинг pattern). Альтернативи:MessageFormat, текстові блоки (Java 15+), або ручний StringBuilder - Lazy evaluation: якщо конкатенація дорога і може не знадобитися — відкладіть її
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- Оператор
+створює НОВИЙ рядок — String незмінний, оригінали не змінюються - Constant folding:
"A" + "B" + "C"→"ABC"на етапі компіляції, 0 операцій у рантаймі - Java 5-8: компілятор замінює
a + b + cнаnew StringBuilder().append(a).append(b).append(c).toString() - Java 9+:
invokedynamic+StringConcatFactory— JVM обирає оптимальну стратегію у рантаймі - У циклах
+створює O(n²) алокацій — використовуйтеStringBuilder null + "text"→"nulltext"— не помилка, але може здивувати
Часті уточнюючі запитання:
- Чому
+у циклі — це погано? — Кожна ітерація створює новий StringBuilder + новий String. Для 1000 ітерацій: 1000 StringBuilder + 1000 String = O(n²). - Що таке
invokedynamicдля конкатенації? — У Java 9+ компілятор генеруєinvokedynamic makeConcatWithConstants. При першому виклику JVM обирає оптимальну стратегію (MH_LF, BC_SB, BH_SB). - Чи оптимізує компілятор цикли з
+? — Ні.invokedynamicоптимізує лише один вираз, не цикл. - Що таке StringConcatFactory? — Фабрика у
java.lang.invoke, яка генерує оптимальний код для конкретної конкатенації.
Червоні прапорці (НЕ говорити):
- ❌ “
+у циклі — нормально” — O(n²) алокацій, використовуйтеStringBuilder - ❌ “Компілятор оптимізує
+у циклі” — оптимізує лише один вираз, не цикл - ❌ “
invokedynamicповільніший заStringBuilder” — для одного виразу — швидший, для циклу — так само погано - ❌ “Конкатенація через
+змінює оригінальні рядки” — String незмінний, завжди створюється новий
Пов’язані теми:
- [[8. Як компілятор Java оптимізує конкатенацію рядків]]
- [[5. Коли використовувати StringBuilder vs StringBuffer]]
- [[4. Чому String є незмінним]]