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

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

В языке Java создать объект строки можно двумя принципиально разными путями: через строковый литерал или через явный вызов конструктора new String(...).


🟢 Junior Level

В языке Java создать объект строки можно двумя принципиально разными путями: через строковый литерал или через явный вызов конструктора new String(...).

Суть в 30 секундах

  • Через строковый литерал (String s = "Java";): JVM автоматически проверяет String Pool (пул строк в куче). Если строка "Java" уже создавалась ранее, JVM не создает новый объект, а переиспользует существующий и возвращает ссылку на него.
  • Через оператор new (String s = new String("Java");): JVM всегда принудительно создает новый объект в обычной куче (Heap), даже если точно такая же строка уже присутствует в String Pool.
// Литералы: переиспользуют объект из пула
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true (ссылаются на один и тот же объект в памяти)

// Конструктор new: всегда создает отдельный объект
String s3 = new String("Hello");
System.out.println(s1 == s3); // false (разные объекты в памяти!)

// Сравнение содержимого через equals всегда возвращает true:
System.out.println(s1.equals(s3)); // true

Сравнительная таблица

Характеристика Литерал (String s = "text") Конструктор (new String("text"))
Где создается объект В String Pool (куча) В Regular Heap (вне пула)
Повторное использование Да (дедупликация на лету) Нет (каждый new создает новый экземпляр)
Расход памяти Минимальный (0 байт, если строка уже есть) Выделение памяти под новый объект заголовка
Скорость выполнения Мгновенно (LDC инструкция) Медленнее (аллокация в куче + вызов <init>)
Рекомендация 100% случаев по умолчанию Только при декодировании byte[] или char[]

🟡 Middle Level

Разбор на уровне байткода JVM

Сравним инструкции байткода Java для обоих подходов с помощью javap -c:

1. Создание через литерал:

String s = "Hello";

Байткод:

0: ldc           #2  // String Hello (загрузка ссылки из Constant Pool)
2: astore_1          // сохранение ссылки в локальную переменную s

Всего одна инструкция ldc! JVM просто берет готовую ссылку на объект из пула констант. Никакого выделения памяти в куче во время выполнения метода не происходит.

2. Создание через оператор new:

String s = new String("Hello");

Байткод:

0: new           #3  // class java/lang/String (выделение сырой памяти в Heap)
3: dup               // дублирование ссылки на вершине стека операндов
4: ldc           #2  // String Hello (загрузка литерала из String Pool!)
6: invokespecial #4  // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1          // сохранение ссылки на новый объект в переменную s

Выполняются 4 тяжелые инструкции: аллокация памяти в Heap (new), дублирование ссылки (dup), загрузка литерала (ldc) и вызов конструктора (invokespecial).

Классический вопрос: сколько объектов создается при new String("Hello")?

Это один из самых частых каверзных вопросов на собеседовании:

Ответ зависит от того, встречался ли литерал "Hello" в JVM ранее!

  1. Если класс только загрузился, и строки "Hello" еще не было в пуле: Создается 2 объекта:
    • Объект №1: строковый литерал "Hello" регистрируется в String Pool (при разрешении константного пула класса).
    • Объект №2: новый объект String создается оператором new в обычной куче (Regular Heap).
  2. Если литерал "Hello" уже присутствовал в String Pool ранее: Создается ровно 1 объект — новый экземпляр String в Regular Heap (литерал просто переиспользуется из пула).

🔴 Senior Level

Как устроен конструктор new String(String original) внутри HotSpot

В исходном коде OpenJDK (Java 9+ с механизмом Compact Strings) конструктор копирования реализован следующим образом:

// java.lang.String (OpenJDK 17/21)
public String(String original) {
    this.value = original.value; // ВНИМАНИЕ: массив байт РАЗДЕЛЯЕТСЯ!
    this.coder = original.coder;
    this.hash = original.hash;
}

Senior-инсайт: Когда вы пишете new String("Hello"), вы НЕ создаете второй массив байт byte[] в памяти!

  • Новый объект в Regular Heap просто ссылается на тот же самый внутренний массив byte[] value, что и литерал в String Pool!
  • Вы тратите оперативную память исключительно на дублирующую «обертку» объекта String:
    • 12 байт — Mark Word и Klass Pointer (при включенном +UseCompressedClassPointers).
    • 4 байта — ссылка на массив value.
    • 4 байта — кэшированный int hash.
    • 1 байт — byte coder (LATIN1 vs UTF16).
    • 1 байт — boolean hashIsZero.
    • 2 байта — padding (выравнивание до кратности 8 байт).
    • Итого: ровно 24 байта в куче впустую на каждый вызов new String()!

Свертка констант (Constant Folding) vs Конкатенация в рантайме

Понимание разницы между созданием в Compile-Time и Runtime критично для предотвращения скрытых утечек производительности:

// Пример 1: Constant Folding (JLS §15.28)
String s1 = "hello world";
String s2 = "hello" + " " + "world"; 
// Компилятор javac сам соединяет литералы во время компиляции!
// В байткоде s2 скомпилируется как ldc "hello world".
System.out.println(s1 == s2); // true! (один и тот же объект в пуле)

// Пример 2: Константы с модификатором final
final String a = "hello";
final String b = " world";
String s3 = a + b; 
// Поскольку переменные a и b объявлены как final и инициализированы литералами,
// javac подставляет их значения во время компиляции!
System.out.println(s1 == s3); // true!

// Пример 3: Обычные переменные (Runtime)
String c = "hello";
String d = " world";
String s4 = c + d; 
// В Java 9+ генерируется инструкция invokedynamic (StringConcatFactory).
// Создается НОВЫЙ объект в Regular Heap (НЕ попадает в пул автоматически).
System.out.println(s1 == s4); // false!
System.out.println(s1 == s4.intern()); // true (intern вернул ссылку из пула)

Исторический контекст: зачем вообще создали new String(String)?

До выхода Java 7 Update 6 (2012 год) реализация String содержала 4 поля:

// Java 6 String implementation:
char[] value;
int offset;
int count;
int hash;
  • Метод substring() в Java 6 не копировал символы, а создавал новый объект String, указывающий на тот же самый гигантский массив char[], просто с другими offset и count.
  • Проблема Memory Leak: Если вы прочитали текстовый файл на 100 МБ в одну строку и сделали String sub = bigString.substring(0, 5), ссылка sub удерживала в памяти весь массив на 100 МБ!
  • Решение в Java 6: Вызывали new String(bigString.substring(0, 5)). Конструктор принудительно аллоцировал новый массив ровно под 5 символов (Arrays.copyOfRange), позволяя GC удалить исходные 100 МБ.
  • В современных версиях Java (7u6+): Метод substring() всегда физически копирует массив символов, поэтому конструктор new String(String) окончательно стал устаревшим антипаттерном.

4 Tricky Questions

1. Сколько объектов создается в коде String s = new String("Java");, и почему ответ может быть как «один», так и «два»?

Ответ:

  • Создается 2 объекта, если строковый литерал "Java" встречается в приложении впервые. Первый объект создается в String Pool при загрузке класса и разрешении его пула констант (Constant Pool). Второй объект создается в Regular Heap оператором new.
  • Создается ровно 1 объект, если класс уже был загружен, или литерал "Java" встречался в коде ранее. В этом случае строковый объект в пуле уже существует, и оператор new создает только один новый объект в обычной куче.

2. Чему будет равен результат s1 == s2 в следующем фрагменте кода, и почему:

final String a = "foo";
final String b = "bar";
String s1 = "foobar";
String s2 = a + b;

Ответ: Выражение вернет true. Разбор: Согласно спецификации языка Java (JLS §15.28 Constant Expressions), если переменные объявлены с модификатором final и инициализированы константными строковыми выражениями во время компиляции, выражение a + b является константным выражением времени компиляции (Compile-Time Constant Expression). Компилятор javac вычисляет значение на этапе сборки и помещает в байткод готовую строку "foobar" (ldc #index). В рантайме переменная s2 ссылается на тот же самый объект из String Pool, что и s1. Если убрать ключевое слово final хотя бы с одной из переменных, конкатенация произойдет в рантайме через invokedynamic, вернется новый объект в куче, и сравнение вернет false.

3. Разделяют ли объекты s1 и s2 один и тот же массив байт byte[] в следующем коде на Java 21:

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

Ответ: Да, разделяют! В исходном коде конструктора public String(String original) поле value не копируется через Arrays.copyOf, а присваивается напрямую: this.value = original.value;. Поскольку строки и их внутренние массивы байт абсолютно иммутабельны (инкапсулированы внутри пакета java.lang), разработчикам JVM незачем тратить память на копирование сырых байт. В результате s1 и s2 представляют собой два разных объекта String в памяти (s1 != s2), но их внутренние поля value указывают на один и тот же массив byte[] в куче!

4. В каких редких практических сценариях в современном Java-коде действительно необходим вызов конструктора new String(...)?

Ответ: Конструктор new String(...) необходим исключительно при преобразовании сырых бинарных или символьных данных в строковый тип:

  1. Декодирование байтов при сетевом/файловом I/O: new String(byteArray, StandardCharsets.UTF_8) — преобразование полученного сетевого пакета в строку с явным указанием кодировки.
  2. Конвертация массивов символов: new String(charArray) — когда символы собирались вручную в буфере.
  3. Безопасная очистка паролей: при работе с char[] password (хотя сами пароли строками представлять не рекомендуется, иногда требуется сформировать одноразовую строку для API). Вызов же конструктора new String(String str) в современном коде не имеет ни одного валидного применения.

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

30-секундный ответ

«Создание через литерал ("Java") использует String Pool: JVM возвращает ссылку на уже существующий объект из пула (инструкция ldc), экономя память. Создание через new String("Java") принудительно выделяет память под новый объект в обычной куче (Regular Heap), игнорируя пул. При этом созданный через new объект разделяет тот же внутренний массив byte[] value, что и литерал, впустую расходуя 24 байта на заголовок объекта. На собеседовании на вопрос «Сколько объектов создается при new String(“text”)?» правильный ответ — два, если литерал встретился впервые, и один, если он уже был в пуле».

Чек-лист ключевых понятий

  1. Литерал: Использует String Pool, 1 инструкция байткода (ldc), 0 аллокаций при повторе.
  2. Конструктор new: Создает новый объект в Regular Heap (s1 != s2, но s1.equals(s2) == true).
  3. Сколько объектов создается: 1 или 2 (в зависимости от наличия литерала в пуле констант).
  4. Внутренний массив байт: Конструктор new String(String) переиспользует массив byte[] value без копирования.
  5. Constant Folding: Конкатенация литералов и final-переменных оптимизируется компилятором javac в константу из пула.

Красные флаги (чего ни в коем случае нельзя говорить)

  • ❌ «new String(“Hello”) всегда создает ровно один объект» (Забывают про создание литерала в String Pool).
  • ❌ «new String(“Hello”) копирует внутренний массив символов» (В HotSpot поле value присваивается по ссылке).
  • ❌ «Конструктор new String(str) нужен для безопасности многопоточности» (Строки иммутабельны изначально, конструктор не добавляет никакой безопасности).

Ссылки на связанные темы