Питання 7 · Розділ 12

Що відбувається при конкатенації рядків через оператор +?

Коли ви використовуєте + для склеювання рядків, Java створює новий рядок з двох (або більше) початкових. Оскільки String незмінний, оригінальні рядки не змінюються.

Мовні версії: English Russian Ukrainian

🟢 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 секцію).

Типові помилки

  1. Помилка: + у циклі Рішення: StringBuilder поза циклом

  2. Помилка: split(".") — крапка це regex-метасимвол Рішення: split("\\.") або split(Pattern.quote("."))

  3. Помилка: 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:

  1. MH_LF (MethodHandle with LambdaForm): Генерує MethodHandle для конкатенації
  2. BC_SB (Bytecode StringBuilder): Генерує байт-код із StringBuilder
  3. BH_SB (Bootstrap method with StringBuilder — compact): Оптимізований варіант

Архітектурні Trade-offs

Плюси invokedynamic:

  • JVM обирає стратегію у рантаймі (adaptivity)
  • Можна додавати нові стратегії без перекомпіляції
  • Менше алокацій — JVM знає всі аргументи і виділяє byte[] потрібного розміру одразу

Мінуси:

  • Bootstrap overhead при першому виклику (~1-5μs)
  • Складніше дебажити байт-код

Edge Cases

  1. null конкатенація: "val: " + null"val: null". StringConcatFactory і StringBuilder викликають String.valueOf(null)"null".

  2. Змішування типів: "Value: " + 42StringConcatFactory знає тип int і використовує оптимальну конвертацію без створення проміжного String.

  3. Цикли: 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 є незмінним]]