Питання 14 · Розділ 12

Чому змінилася реалізація substring() в Java 7?

Розробники Java змінили substring() у версії 7 update 6, тому що стара версія викликала приховані витоки пам'яті.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Розробники Java змінили substring() у версії 7 update 6, тому що стара версія викликала приховані витоки пам’яті.

Проблема: У старій версії substring() не копіював дані, а посилався на той самий масив, що і оригінальний рядок. Якщо ви взяли маленький підрядок з величезного тексту, весь величезний текст залишався в пам’яті.

Приклад:

// Java 6 — ВЕЛИЧЕЗНИЙ рядок (наприклад, вміст файлу)
String huge = loadBigFile(); // 100 МБ
// Беремо маленьку частину — тільки 10 символів
String small = huge.substring(0, 10);
huge = null; // "Видалили" великий рядок
// АЛЕ: в пам'яті все ще 100 МБ, тому що small посилається на той самий масив!

Рішення: В Java 7+ substring() завжди копіює потрібні дані. Маленький підрядок займає стільки пам’яті, скільки повинен.


🟡 Middle Level

Що було до Java 7u6

Об’єкт String містив три поля:

  • char[] value — масив символів
  • int offset — початок рядка
  • int count — довжина

substring() створював новий String з тим самим value, але іншими offset і count.

Що змінилося

Починаючи з Java 7u6:

  • Поля offset і count видалені з String
  • substring() завжди створює новий масив і копіює дані
  • Заголовок об’єкта String став меншим (економія пам’яті для ВСІХ рядків)

Практичне застосування

// Java 7+ — безпечно
String huge = loadBigFile();     // 100 МБ
String small = huge.substring(0, 10); // ~50 байт (копія!)
huge = null;                     // 100 МБ звільнені для GC

Типові помилки

  1. Помилка: Очікування O(1) продуктивності Рішення: substring() — O(n), копіює дані

  2. Помилка: Використання new String(substring) як “оптимізація” Рішення: Це було потрібно в Java 6. У сучасних Java — зайва алокація


🔴 Senior Level

Internal Implementation — мотивація змін

JDK-7068364: Офіційний bug report в Oracle.

Проблеми shared-array підходу:

  1. Memory leak: Підрядок утримує весь батьківський масив
  2. Complexity: Три поля (value, offset, count) замість одного
  3. GC overhead: Один великий масив, на який посилається безліч підрядків — складніше для GC алгоритмів

Архітектурні Trade-offs

До Java 7u6:

String Object (Java 6):
├── char[] value (reference) ──┐
├── int offset                  │  Shared char[]
├── int count                   │  [H][e][l][l][o][,][ ][W][o][r][l][d]...
└── int hash                    │  (може бути 10MB+)

Після Java 7u6:

String Object (Java 7+):
├── char[] value ──→ [W][o][r][l][d]  (тільки підрядок)
└── int hash

Чому жертва продуктивності виправдана

Метрика Java 6 (shared) Java 7+ (copying)
substring() час O(1) O(n)
substring() пам’ять 0 extra bytes O(n) bytes
String object size 32 bytes 24 bytes
Memory leak risk High None
GC friendliness Poor Good

Ключовий інсайт: У типових додатках substring() викликається для рядків розумного розміру (< 1KB). O(n) копіювання для таких рядків — це наносекунди. А memory leak із shared array — це OOM у продакшені.

Edge Cases

  1. Legacy workaround більше не потрібен:
    // Java 6 workaround:
    String copy = new String(original.substring(0, 10));
    
    // Java 7+: substring() вже копіює, new String() — зайва алокація
    String copy = original.substring(0, 10);
    
  2. Java 9+ Compact Strings: Копіювання стало ще ефективнішим — byte[] замість char[] економить 50% для латинських рядків.

  3. Zero-copy альтернативи: Якщо критична продуктивність без копіювання:
    • CharSequence wrapper — не копіює дані
    • java.nio.CharBuffer — view на масив
    • Сторонні бібліотеки: StringView (Guava), Slice

Production Experience

Сценарій: Парсер XML (Java 6):

  • Витягнення text content з елементів
  • Кожен <description> — 50KB, підрядок — 200 символів
  • 1M елементів → 1M підрядків → кожен тримає посилання на 50KB масив батьківського рядка.
  • Retained size totals 50GB — додаток OOM задовго до досягнення 1M елементів.
  • Fix: new String(element.getText().substring(...)) → 200 bytes per substring

Сценарій: Міграція Java 8 → 17:

  • Код вже використовує copying substring() — міграція прозора
  • Compact Strings (Java 9+) дали додатково -50% пам’яті на підрядки
  • Жодних змін у поведінці — drop-in replacement

Performance Benchmarks

| Операція | Java 6 | Java 8 | Java 17 | | ————————– | ————- | ——– | ————- | | substring(0, 10) з 1KB | ~1ns | ~5ns | ~3ns (Latin1) | | substring(0, 100) з 1KB | ~1ns | ~20ns | ~12ns | | substring(0, 10) з 1MB | ~1ns | ~500ns | ~250ns | | GC impact | High (shared) | Low | Low |

// Бенчмарки приблизні. Реальні значення залежать від JVM, CPU і warmup.

Best Practices для Highload

  • У сучасних Java: substring() безпечний — використовуйте без остраху
  • Для zero-copy: CharSequence wrappers або CharBuffer
  • Для парсингу величезних файлів: stream processing, не завантажуйте все в String
  • Не використовуйте new String(substring()) — це redundant в Java 7+

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Причина змін: memory leak — підрядок утримував весь масив батьківського рядка
  • JDK-7068364 — офіційний bug report в Oracle
  • До Java 7u6: String мав 3 поля (value, offset, count), після — тільки value
  • Trade-off: O(1) → O(n) за часом, але memory leak risk → none
  • Видалення offset і count зменшило розмір об’єкта String на 8 байт
  • В Java 9+ компактні рядки (byte[]) зробили копіювання ще ефективнішим

Часті уточнюючі запитання:

  • Чому пожертвували продуктивністю? — У типових додатках substring() для рядків < 1KB — наносекунди. А memory leak — це OOM у продакшені.
  • Чи потрібен legacy workaround new String(substring) зараз? — Ні, у сучасних Java substring() вже копіює, new String() — зайва алокація.
  • Які zero-copy альтернативи є?CharSequence wrapper, CharBuffer, Guava StringView.
  • Як змінився розмір об’єкта String? — Став меншим: прибрали offset і count — економія ~8 байт на кожен рядок.

Червоні прапорці (НЕ говорити):

  • ❌ “Змінили просто так, без причини” — була критична витік пам’яті
  • ❌ “substring() досі O(1)” — O(n) з Java 7u6
  • ❌ “Потрібно використовувати new String(substring()) для безпеки” — redundant в Java 7+
  • ❌ “Зміни зламали зворотну сумісність” — поведінка substring() для користувача не змінилася

Пов’язані теми:

  • [[13. Що робить метод substring() і як він працював до Java 7]]
  • [[19. Що таке компактні рядки в Java 9+]]
  • [[20. Як дізнатися, скільки пам’яті займає String]]
  • [[4. Чому String є незмінним]]