В чому різниця між створенням 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 проти UTF16). - 1 байт —
boolean hashIsZero. - 2 байти — padding (вирівнювання до кратності 8 байтів).
- Разом: рівно 24 байти в купі даремно на кожен виклик
new String()!
- 12 байтів — Mark Word та Klass Pointer (при увімкненому
Згортання констант (Constant Folding) проти Конкатенації в рантаймі
Розуміння різниці між створенням у 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 поля:
// Реалізація String у Java 6:
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) потрібен для безпеки багатопоточності» (Рядки імутабельні від самого початку, конструктор не додає жодної безпеки).