Чому String є незмінним (незмінюваним)?
Кожна модифікація (concat, replace, substring) створює новий об'єкт — це генерує GC pressure. У low-latency системах для гарячих шляхів використовують StringBuilder або byte[] н...
🟢 Junior Level
Незмінність означає, що після створення об’єкта String його вміст не можна змінити. Жоден метод не може поміняти символи всередині рядка — замість цього створюється новий рядок.
Чому це важливо:
- Безпека — паролі та шляхи до файлів не можна підмінити
- Потокобезпека — рядки можна передавати між потоками без синхронізації
- String Pool — можливий лише тому, що рядки не можна змінювати
Приклад:
String s = "Hello";
s.toUpperCase(); // Створюється НОВИЙ рядок "HELLO"
System.out.println(s); // Виведе "Hello" — оригінал не змінився!
Проста аналогія: Рядок — як надрукована книга. Ви не можете змінити текст у вже надрукованій книзі — замість цього потрібно надрукувати нову.
🟡 Middle Level
Ціна незмінності
Кожна модифікація (concat, replace, substring) створює новий об’єкт — це генерує GC pressure. У low-latency системах для гарячих шляхів використовують StringBuilder або byte[] напряму.
Як це реалізовано в JDK
finalклас:public final class String— не можна успадкувати і перевизначити методиfinalполе:private final byte[] value(Java 9+) — посилання на масив не може бути змінене- Немає мутаторів: Жодного методу типу
setCharAt(), що змінює вміст - Захисне копіювання: Методи
substring(),replace(),concat()повертають нові об’єкти
Де використовується на практиці
- Ключі HashMap/HashSet: hashCode() кешується при першому виклику і ніколи не змінюється
- Параметри безпеки: шляхи до файлів, URL, credentials — не можна змінити після перевірки
- Багатопотоковість: рядки передаються між потоками без будь-яких блокувань
Типові помилки
-
Помилка: Очікування, що
replace()змінить рядок на місці Рішення: Запам’ятовуйте результат:s = s.replace("a", "b"); -
Помилка: Конкатенація в циклі через
+Рішення: ВикористовуйтеStringBuilder— кожна конкатенація створює новий об’єкт
Порівняння з альтернативами
| Тип | Незмінний | Потокобезпечний | Для модифікацій | | ————- | ———— | —————– | ——————– | | String | Так | Так | Ні | | StringBuilder | Ні | Ні | Так (один потік) | | StringBuffer | Ні | Так (synchronized)| Так (багато потоків) |
🔴 Senior Level
Internal Implementation
// OpenJDK — структура String (Java 9+)
public final class String implements Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value; // COMPACT STRINGS: byte[] замість char[]
private final byte coder; // LATIN1=0 або UTF16=1
private int hash; // Кеш hashCode, 0 за замовчуванням
// ...
}
Анотація @Stable (JVM intrinsic) повідомляє JIT-компілятору, що поле value не буде змінене після конструктора. Це дозволяє агресивні оптимізації: constant folding, escape analysis.
Архітектурні Trade-offs
Чому не mutable:
- Security: Рядки використовуються в
ClassLoader,SecurityManager, мережевих підключеннях, SQL-запитах. Якби рядки були мутабельними:// Зловмисник міг би: checkAccess("/admin/config"); // Перевірка пройшла // ... змінити рядок у пам'яті ... readFile("/admin/config"); // Читання іншого файлу! - String Pool: Якби рядки були мутабельними, зміна
"Hello"в одному місці змінила б усі посилання на цей літерал у всьому додатку.
Якби рядки були мутабельними, то зміна s1 змінила б і s2, тому що вони посилаються на один об’єкт у пулі. Незмінність гарантує, що один рядок у пулі безпечний для всіх, хто на нього посилається.
String s1 = "Hello";
String s2 = "Hello";
// s1 і s2 — один об'єкт. Якби s1 можна було змінити, s2 теж змінився б.
-
HashMap stability: Мутабельний ключ у HashMap — втрата даних при зміні hashCode.
-
Thread safety: Незмінні об’єкти thread-safe by design. Жодних race conditions, жодних
volatile, жодних блокувань.
Edge Cases
-
Рефлексія: До Java 9 можна було змінити
valueчерезField.setAccessible(true). У Java 9 з’явився модульний доступ (JEP 261), а з Java 16 (JEP 396) strong encapsulation увімкнено за замовчуванням —--add-opensпотрібен для доступу до внутрішніх полів. -
StringBuilder під капотом:
String.format(),String.join()internally використовують mutable буфери, але результат — завжди незмінний String. -
String concat через invokedynamic (Java 9+):
StringConcatFactoryгенерує ефективний код, але результат все одно незмінний.
Продуктивність
- Алокація: Новий String (Java 9+, Latin1, 10 символів) ≈ 48 байт (об’єкт + byte[])
- GC: Short-lived об’єкти рядків ефективно обробляються Young Gen (Eden → Survivor → смерть)
- Кеш hashCode: Обчислюється ліниво один раз, потім O(1) доступ
Production Experience
Сценарій: Логування у високонавантаженому сервісі (100K req/sec):
- Кожен рядок логу створює 3-5 тимчасових String об’єктів
- Без Compact Strings: 500MB/sec алокацій → Minor GC кожні 200ms
- З Compact Strings (Java 9+): 250MB/sec → Minor GC кожні 400ms
- Рішення: структуроване логування (byte[] напряму у logging framework)
Monitoring
// JOL — реальний розмір String
System.out.println(GraphLayout.parseInstance("Hello").toPrintable());
// java.lang.String object internals:
// OFFSET SIZE TYPE DESCRIPTION
// 0 12 (object header)
// 12 4 byte[] String.value
// 16 1 byte String.coder
// ...
// Total: 24 bytes + array size
Best Practices для Highload
- Для частих модифікацій:
StringBuilderз попередньо вказаним capacity - Для міжпотокового обміну:
String(thread-safe без блокувань) - Для парсингу великих текстів: працюйте з
byte[]/CharBufferнапряму, конвертуйте в String лише коли потрібно - Розгляньте
byte[]+coderпатерн для ultra-low-latency систем
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- Незмінність = після створення вміст не можна змінити, усі модифікації створюють новий об’єкт
- Реалізація:
finalклас,final byte[] value,@Stableанотація, немає мутаторів - String Pool можливий лише завдяки незмінності — інакше зміна одного рядка торкнулася б усіх посилань
- Thread-safe by design — жодних блокувань, race conditions неможливі
- Безпека: рядки використовуються в SecurityManager, ClassLoader, SQL-запитах — mutable рядок = уразливість
- Ключі HashMap: hashCode() кешується і ніколи не змінюється
Часті уточнюючі запитання:
- Чому String immutable? — 4 причини: (1) String Pool — неможливий для mutable рядків, (2) Thread safety — без блокувань, (3) Security — шляхи, URL, credentials не можна підмінити, (4) HashMap stability — hashCode не змінюється.
- Яка ціна незмінності? — GC pressure при модифікаціях. Кожна конкатенація/replace створює новий об’єкт.
- Чи можна змінити String через рефлексію? — Технічно так, але це порушення контракту, ламає String Pool, HashMap, JIT-оптимізації.
- Що використовувати для модифікацій? —
StringBuilder(один потік),StringBuffer(кілька потоків),byte[](low-level).
Червоні прапорці (НЕ говорити):
- ❌ “
replace()змінює рядок на місці” — повертає новий рядок - ❌ “String immutable означає не можна створити новий рядок” — можна, оригінал не змінюється
- ❌ “String thread-safe тому що
finalклас” — thread-safe тому що стан не змінюється - ❌ “Рефлексія — нормальний спосіб змінити String” — це антипатерн, порушує контракт
Пов’язані теми:
- [[5. Коли використовувати StringBuilder vs StringBuffer]]
- [[1. Як працює String Pool]]
- [[21. Чи можна змінити вміст String через рефлексію]]