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

Що робить метод substring() і як він працював до Java 7?

Метод substring() повертає частину рядка — підрядок від вказаного індексу до кінця або до іншого індексу.

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

🟢 Junior Level

Метод substring() повертає частину рядка — підрядок від вказаного індексу до кінця або до іншого індексу.

Приклад:

String text = "Hello, World!";
String sub = text.substring(0, 5); // "Hello"
String sub2 = text.substring(7);   // "World!"

Важлива деталь: У старих версіях Java (до 7u6) substring() працював хитро — він не копіював дані, а посилався на той самий масив символів, що і оригінальний рядок. Це викликало проблеми з пам’яттю.

У сучасній Java substring() завжди створює копію потрібних символів — це безпечно і передбачувано.


🟡 Middle Level

Як працює substring() зараз (Java 7u6+)

String text = "Hello, World!";
String sub = text.substring(7, 12); // "World"

Сучасна реалізація:

  1. Обчислює довжину підрядка
  2. Створює новий масив byte[] (Java 9+) або char[] (Java 7-8)
  3. Копіює тільки потрібні дані
  4. Повертає новий об’єкт String

Складність: O(n) — пропорційно довжині підрядка (копіювання даних).

Як працювало до Java 7u6

До Java 7u6 об’єкт String містив:

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

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

Плюс: O(1) — миттєво, без копіювання. Мінус: Витік пам’яті — маленький підрядок утримує величезний масив батьківського рядка.

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

  1. Помилка: substring(0, 5) для рядка довжиною 3 символи Рішення: Перевіряйте межі або використовуйте Math.min

  2. Помилка: Очікування, що substring() змінить оригінал Рішення: substring() повертає новий рядок, оригінал не змінюється


🔴 Senior Level

Internal Implementation

До Java 7u6 (shared array):

// JDK 6
String(int offset, int count, char[] value) {
    this.value = value;     // Shared reference!
    this.offset = offset;
    this.count = count;
}

String substring(int beginIndex, int endIndex) {
    // Перевіряємо межі
    // Створюємо новий String з тим самим char[] value
    return new String(offset + beginIndex, endIndex - beginIndex, value);
}

Java 7u6 — Java 8 (copying):

// JDK 7u6+
String substring(int beginIndex, int endIndex) {
    // Перевіряємо межі
    int subLen = endIndex - beginIndex;
    // Копіюємо дані в новий масив
    return new String(value, beginIndex, subLen);
    // new String(...) викликає Arrays.copyOfRange
}

Java 9+ (Compact Strings):

// JDK 9+ — byte[] замість char[]
String substring(int beginIndex, int endIndex) {
    // Перевіряємо межі
    int subLen = endIndex - beginIndex;
    // Копіюємо байти з урахуванням coder (LATIN1/UTF16)
    return isLatin1()
        ? StringLatin1.newString(value, beginIndex, subLen)
        : StringUTF16.newString(value, beginIndex, subLen);
}

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

Старий підхід (shared array):

  • Плюси: O(1), zero-copy, економія пам’яті при безлічі підрядків
  • Мінуси: Memory leak — підрядок з 5 символів утримує 10MB батьківського рядка

Новий підхід (copying):

  • Плюси: Передбачувана пам’ять, оригінал може бути GC’d
  • Мінуси: O(n) — копіювання даних, більше алокацій

Edge Cases

  1. Memory Leak (Java 6):
    String huge = readLargeFile(); // 100MB
    String small = huge.substring(0, 10); // 10 chars
    huge = null; // "Видалили" huge
    // АЛЕ: small.value все ще посилається на 100MB масив!
    

    Workaround в Java 6: new String(huge.substring(0, 10)) — примусова копія.

  2. IndexOutOfBoundsException:
    "abc".substring(0, 5); // кине exception
    
  3. Empty substring:
    "abc".substring(2, 2); // "" — порожній рядок (не null!)
    

Продуктивність

| Операція | Java 6 (shared) | Java 7+ (copying) | Java 9+ (compact) | | —————— | —————- | ——————- | ——————- | | substring(0, 10) | O(1), 0 bytes | O(n), ~48 bytes | O(n), ~34 bytes | | substring з 1MB | O(1), 0 bytes | O(n), ~2MB alloc | O(n), ~1MB (Latin1) | | Memory leak risk | High | None | None |

// ~48 bytes = String header (24) + byte[] header (16) + 10 bytes data, rounded up. // Java 6: ~20000 bytes — це розмір shared char[] батьківського рядка (10,000 chars * 2 bytes), // а не самого substring-об’єкта.

Production Experience

Сценарій: Парсинг логів (Java 6):

  • Витягнення requestId (36 chars) з 10MB log line
  • 100K запитів → 100K підрядків → кожен тримає 10MB → OOM
  • Fix: new String(line.substring(0, 36)) — примусова копія

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

  • В Java 8 substring() копіював char[] (2 bytes/char)
  • В Java 17 substring() копіює byte[] (1 byte/char для Latin1)
  • Результат: -50% memory для підрядків латинського тексту

Monitoring

// JOL — реальний розмір підрядка
String huge = "A".repeat(10000);
String sub = huge.substring(0, 5);
System.out.println(GraphLayout.parseInstance(sub).toFootprint());
// Java 7+: ~48 bytes (own copy)
// Java 6:  ~20000 bytes (shares parent's array!)

Best Practices для Highload

  • У сучасній Java: substring() безпечний — завжди копіює
  • Для zero-copy парсингу: працюйте з CharSequence, CharBuffer, або кастомними StringView
  • Якщо потрібна підрядка з величезного тексту і батьківський більше не потрібен: substring() автоматично звільнить батьківський масив при GC
  • Розгляньте text.substring() + intern() для часто повторюваних підрядків

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

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

  • substring(begin, end) повертає підрядок від begin до end (не включаючи end)
  • До Java 7u6: substring() шарив char[] батьківського рядка — O(1), але викликав memory leak
  • Java 7u6+: substring() копіює дані — O(n), але безпечно
  • Java 9+: копіює byte[] з урахуванням coder (Latin-1/UTF-16)
  • Memory leak в Java 6: маленький підрядок утримував величезний масив батьківського рядка
  • Workaround в Java 6: new String(substring()) — примусова копія

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

  • Чому змінили substring() в Java 7? — Стара версія викликала приховані витоки пам’яті: підрядок з 5 символів утримував 10MB батьківського рядка.
  • Яка складність substring() зараз? — O(n) — копіює дані. Не O(1) як раніше.
  • Чи потрібен new String(substring()) у сучасній Java? — Ні, це зайва алокація. substring() вже копіює.
  • Що буде при substring() за межами рядка?IndexOutOfBoundsException.

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

  • ❌ “substring() працює за O(1)” — тільки в Java 6, зараз O(n)
  • ❌ “new String(substring()) — оптимізація” — це була необхідність в Java 6, зараз redundant
  • ❌ “substring() змінює оригінал” — String незмінний, завжди повертається новий рядок
  • ❌ “Memory leak від substring() — актуальна проблема” — виправлено в Java 7u6

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

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