Вопрос 2 · Раздел 12

В чём разница между созданием String через литерал и через new?

Создать строку в Java можно двумя способами, и они работают по-разному:

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Создать строку в Java можно двумя способами, и они работают по-разному:

Через литерал (рекомендуется):

String s = "Hello";

JVM проверяет String Pool. Если такая строка уже есть — возвращает ссылку на неё. Если нет — создаёт новую в пуле.

Через конструктор new (не рекомендуется):

String s = new String("Hello");

Всегда создаёт новый объект в обычной куче, игнорируя String Pool для самого объекта.

Пример:

String s1 = "Hello";
String s2 = "Hello";
String s3 = new String("Hello");

System.out.println(s1 == s2); // true  (один объект в пуле)
System.out.println(s1 == s3); // false (s3 — отдельный объект)
System.out.println(s1.equals(s3)); // true (содержимое одинаковое)

Когда использовать: В 99% случаев используйте литералы. Конструктор new String(...) нужен в основном при конвертации из byte[]/char[], а также в редких случаях для создания гарантированно независимой копии (defensive copying).


🟡 Middle Level

Как это работает внутри

Литерал:

String s = "Hello";
  • Байт-код: инструкция LDC (Load Constant)
  • JVM проверяет String Pool по хешу строки
  • Если найдена — возвращает ссылку, если нет — создаёт объект в пуле
  • Оптимизация происходит на этапе загрузки класса

Конструктор:

String s = new String("Hello");
  • Байт-код: NEWDUPLDC "Hello"INVOKESPECIAL
  • Литерал "Hello" уже находится в String Pool
  • new String(...) создаёт второй объект в Regular Heap с тем же содержимым
  • Результат: два объекта в памяти с одинаковым текстом

Типичные ошибки

  1. Ошибка: new String("literal") для “гарантии уникальности” Решение: Это антипаттерн. Уникальность объектов строк почти никогда не нужна

  2. Ошибка: Думать, что new String() быстрее Решение: Литералы быстрее — они используют пул и не создают лишних объектов

Когда конструктор всё же нужен

| Случай | Пример | Объяснение | | ———————– | ————————– | ————————– | | Из byte[] | new String(bytes, UTF_8) | Основной способ при I/O | | Из char[] | new String(chars) | Конвертация символов | | Из StringBuffer/Builder | builder.toString() | Неявный вызов конструктора |


🔴 Senior Level

Internal Implementation

Байт-код литерала:

0: ldc           #2  // String Hello
2: astore_1

Одна инструкция — загрузка константы из Constant Pool класса.

Байт-код конструктора:

0: new           #3  // class java/lang/String    // NEW — выделяет память для нового объекта String
3: dup                                        // DUP — дублирует ссылку для конструктора
4: ldc           #2  // String Hello            // LDC — загружает литерал из Constant Pool
6: invokespecial #4  // Method ..."<init>":...  // INVOKESPECIAL — вызывает конструктор String
9: astore_1

Четыре инструкции: выделение памяти, дублирование ссылки, загрузка константы, вызов конструктора.

Архитектурные Trade-offs

Почему конструктор существует:

  • Исторически: для создания строк из сырых данных (byte[], char[], int[] кодовых точек)
  • Практически: new String(String original) копирует содержимое в новый массив (в Java 7+), гарантируя независимость от оригинала

Зачем может понадобиться независимая копия:

  • До Java 7u6: substring() разделял char[] родителя. new String(substring) копировал данные, освобождая родительский массив для GC
  • В современных Java это неактуально — substring() всегда копирует данные

Edge Cases

  1. String с intern():
    String s = new String("Hello").intern();
    

    Создаёт объект в Heap, затем intern() проверяет пул. Если "Hello" уже в пуле — возвращает ссылку из пула, а объект из Heap становится мусором.

  2. Constant Folding:
    String s = "Hel" + "lo"; // Компилятор схлопывает в "Hello" на этапе компиляции
    

    Результат будет в String Pool, как если бы написали "Hello".

  3. Runtime конкатенация:
    String a = "Hel";
    String s = a + "lo"; // НЕ в пуле! Создаётся через invokedynamic/StringBuilder
    

Производительность

| Операция | Аллокации | Время | GC pressure | | ——————— | ————————- | ——————– | ————- | | "Hello" | 0 (если в пуле) | ~0 | None | | new String("Hello") | 1 объект + литерал в пуле | ~10-20ns | Medium | | "Hello".intern() | 0 | Lookup в хеш-таблице | Low |

Production Experience

В парсерах JSON/XML, где одно и то же поле ("name", "id", "type") встречается миллионы раз:

  • Без пула: миллионы объектов String → значительный GC overhead
  • С пулом: десятки уникальных строк → экономия 90%+ памяти на строках

Monitoring

# Посмотреть количество объектов String
jmap -histo:live <pid> | grep java.lang.String

# Анализ через JOL
System.out.println(GraphLayout.parseInstance(s1).toFootprint());
System.out.println(GraphLayout.parseInstance(s2).toFootprint());

Best Practices

  • Никогда не используйте new String("literal") без обоснованной причины
  • Для перевода строки из Heap в Pool используйте .intern()
  • Если нужна гарантированно независимая копия — new String(existingString) (хотя это редко требуется)

🎯 Шпаргалка для интервью

Обязательно знать:

  • Литерал String s = "Hello" — JVM проверяет String Pool, возвращает ссылку из пула или создаёт в пуле
  • new String("Hello") — всегда создаёт НОВЫЙ объект в куче (даже если литерал уже в пуле)
  • Литерал использует байт-код LDC, конструктор — NEW + DUP + LDC + INVOKESPECIAL
  • Constant folding: "Hel" + "lo" компилятор схлопывает в "Hello" на этапе компиляции
  • Runtime конкатенация (a + "lo") — НЕ в пуле, создаётся через invokedynamic/StringBuilder
  • new String("...") создаёт 2 объекта: литерал в пуле + объект в Heap

Частые уточняющие вопросы:

  • Сколько объектов создаёт new String("Hello")? — Один или два. Литерал "Hello" уже в пуле (при загрузке класса), new String() создаёт второй объект в Heap.
  • Зачем вообще существует конструктор new String(String)? — Для создания независимой копии (defensive copying). До Java 7u6 это было нужно для освобождения памяти от shared array.
  • Что быстрее: литерал или new String()? — Литерал быстрее — 0 аллокаций (если уже в пуле). new String() — ~10-20ns + GC pressure.
  • Когда new String() из byte[]/char[] нужен? — Это основной способ при I/O: new String(bytes, UTF_8), new String(chars).

Красные флаги (НЕ говорить):

  • ❌ “new String("literal") создаёт уникальную строку” — создаёт дубликат, а не уникальность
  • ❌ “Литерал и new String() — одно и то же” — разное размещение в памяти
  • ❌ “new String() быстрее литерала” — наоборот, литерал быстрее
  • ❌ “Constant folding работает для переменных” — только для static final констант и литералов

Связанные темы:

  • [[1. Как работает String Pool]]
  • [[3. Когда стоит использовать intern()]]
  • [[9. Можно ли использовать == для сравнения String]]
  • [[7. Что происходит при конкатенации строк через оператор +]]