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