Що робить метод 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. Що таке компактні рядки в Java 9+]]
- [[20. Як дізнатися, скільки пам’яті займає String]]