В чём разница между созданием 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 ранее!
- Если класс только загрузился, и строки
"Hello"еще не было в пуле: Создается 2 объекта:- Объект №1: строковый литерал
"Hello"регистрируется в String Pool (при разрешении константного пула класса). - Объект №2: новый объект
Stringсоздается операторомnewв обычной куче (Regular Heap).
- Объект №1: строковый литерал
- Если литерал
"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()!
- 12 байт — Mark Word и Klass Pointer (при включенном
Свертка констант (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(...) необходим исключительно при преобразовании сырых бинарных или символьных данных в строковый тип:
- Декодирование байтов при сетевом/файловом I/O:
new String(byteArray, StandardCharsets.UTF_8)— преобразование полученного сетевого пакета в строку с явным указанием кодировки. - Конвертация массивов символов:
new String(charArray)— когда символы собирались вручную в буфере. - Безопасная очистка паролей: при работе с
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”)?» правильный ответ — два, если литерал встретился впервые, и один, если он уже был в пуле».
Чек-лист ключевых понятий
- Литерал: Использует String Pool, 1 инструкция байткода (
ldc), 0 аллокаций при повторе. - Конструктор new: Создает новый объект в Regular Heap (
s1 != s2, ноs1.equals(s2) == true). - Сколько объектов создается: 1 или 2 (в зависимости от наличия литерала в пуле констант).
- Внутренний массив байт: Конструктор
new String(String)переиспользует массивbyte[] valueбез копирования. - Constant Folding: Конкатенация литералов и
final-переменных оптимизируется компиляторомjavacв константу из пула.
Красные флаги (чего ни в коем случае нельзя говорить)
- ❌ «new String(“Hello”) всегда создает ровно один объект» (Забывают про создание литерала в String Pool).
- ❌ «new String(“Hello”) копирует внутренний массив символов» (В HotSpot поле
valueприсваивается по ссылке). - ❌ «Конструктор new String(str) нужен для безопасности многопоточности» (Строки иммутабельны изначально, конструктор не добавляет никакой безопасности).