Вопрос 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. Что такое compact strings в Java 9+]]
  • [[20. Как узнать, сколько памяти занимает String]]