Вопрос 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, а когда StringBuffer]]
  • [[4. Почему String является иммутабельным]]