Питання 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. Що відбувається при конкатенації рядків через оператор +]]