Что делает метод substring() и как он работал до Java 7?
Метод substring() возвращает часть строки — подстроку от указанного индекса до конца или до другого индекса.
🟢 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"
Современная реализация:
- Вычисляет длину подстроки
- Создаёт новый массив
byte[](Java 9+) илиchar[](Java 7-8) - Копирует только нужные данные
- Возвращает новый объект
String
Сложность: O(n) — пропорционально длине подстроки (копирование данных).
Как работало до Java 7u6
До Java 7u6 объект String содержал:
char[] value— ссылка на массив символовint offset— начало строки в массивеint count— длина строки
substring() создавал новый String с теми же value, но другими offset и count.
Плюс: O(1) — мгновенно, без копирования. Минус: Утечка памяти — маленькая подстрока удерживает огромный массив родителя.
Типичные ошибки
-
Ошибка:
substring(0, 5)для строки длиной 3 символа Решение: Проверяйте границы или используйтеMath.min -
Ошибка: Ожидание, что
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
- 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))— принудительная копия. - IndexOutOfBoundsException:
"abc".substring(0, 5); // Бросит exception - 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]]