Вопрос 15 · Раздел 12

Как работает метод split()?

Метод split() разбивает строку на массив подстрок по заданному разделителю.

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Метод split() разбивает строку на массив подстрок по заданному разделителю.

Простой пример:

String data = "apple,banana,cherry";
String[] fruits = data.split(",");
// ["apple", "banana", "cherry"]

Важно: Разделитель — это регулярное выражение, а не просто строка! Некоторые символы нужно экранировать:

String ip = "192.168.1.1";
String[] parts = ip.split("\\."); // Точку нужно экранировать!
// ["192", "168", "1", "1"]

Пустые строки в конце по умолчанию удаляются:

"a,b,,,".split(",");  // ["a", "b"] — пустые убраны
"a,b,,,".split(",", -1); // ["a", "b", "", "", ""] — все сохранены

🟡 Middle Level

Две версии метода

String[] split(String regex)           // limit = 0
String[] split(String regex, int limit) // полный контроль

Параметр limit: | Limit | Поведение | Пример "a,b,c,,".split(",", limit) | | ————- | —————————————————– | ————————————- | | 0 (default) | Максимальное разделение, пустые в конце удаляются | ["a", "b", "c"] | | > 0 | Не более limit элементов, остаток — в последний | ["a", "b,c,,"] (limit=2) | | < 0 | Максимальное разделение, пустые сохраняются | ["a", "b", "c", "", ""] |

Быстрый путь (Fast Path) оптимизация

split() НЕ всегда использует тяжёлый regex-движок. Если разделитель — один символ (не regex-метасимвол), используется прямой поиск:

// Fast Path — нет компиляции regex
"hello world".split(" ");

// Regex engine — компиляция Pattern/Matcher
"hello world".split("\\s+");
Метасимволы, которые ломают Fast Path: ., $, , (, ), [, ], ^, ?, *, +, \

Типичные ошибки

  1. Ошибка: split(".") — точка = “любой символ” в regex Решение: split("\\.") или split(Pattern.quote("."))

  2. Ошибка: Ожидание пустых строк в конце по умолчанию Решение: Используйте split(",", -1) для сохранения пустых

  3. Ошибка: split() в цикле для одной и той же regex Решение: Скомпилируйте Pattern один раз: Pattern.compile(",").split(str)


🔴 Senior Level

Internal Implementation

OpenJDK — String.split():

public String[] split(String regex, int limit) {
    char ch = 0;
    // Fast Path: один символ, не regex meta
    if (((regex.value.length == 1 &&
         ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
         (regex.length() == 2 &&
          regex.charAt(0) == '\\' &&
          (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
          ((ch-'a')|('z'-ch)) < 0 &&
          ((ch-'A')|('Z'-ch)) < 0)) &&
        (ch < Character.MIN_HIGH_SURROGATE ||
         ch > Character.MAX_LOW_SURROGATE)) {
        // Это проверяет, что ch НЕ является строчной буквой (аналогично для верхнего
        // регистра и цифр) — гарантируя, что \ не является известным shorthand (\d, \w и т.д.)
        // FAST PATH — прямой поиск по массиву байт
        int off = 0;
        int next = indexOf(ch, off);
        // ... manual splitting без Pattern/Matcher
    }
    // SLOW PATH — через Pattern
    return Pattern.compile(regex).split(this, limit);
}

Архитектурные Trade-offs

Fast Path:

  • Плюсы: Нет аллокации Pattern/Matcher, ~5-10ns на вызов
  • Минусы: Работает только для простейших разделителей

Regex Engine (Pattern/Matcher):

  • Плюсы: Полная мощность регулярных выражений
  • Минусы: Компиляция regex (~1-5μs), аллокации Pattern + Matcher + results

Edge Cases

  1. Empty input:
    "".split(",");    // [""] — массив с одной пустой строкой
    ",".split(",");   // [] — пустой массив (limit=0 удаляет пустые)
    ",".split(",", -1); // ["", ""] — две пустые строки
    
  2. Regex с lookahead/lookbehind:
    "a1b2c3".split("(?=\\d)"); // ["a", "1b", "2c", "3"] — split перед цифрой
    
  3. Trailing empty strings:
    "a,,b".split(",");     // ["a", "", "b"]
    "a,,b,,,".split(",");  // ["a", "", "b"] — trailing убраны
    "a,,b,,,".split(",", -1); // ["a", "", "b", "", "", ""]
    

Производительность

| Сценарий | Fast Path | Regex Engine | Pre-compiled Pattern | | | ———————- | ———– | ————- | ——————— | —– | | split(",") 1M раз | ~50ms | ~500ms | ~80ms | | | split("\\ | ") 1M раз | ~60ms | ~500ms | ~80ms | | split("\\s+") 1M раз | N/A | ~800ms | ~120ms | | | Regex compile overhead | 0 | ~1-5μs | 0 (once) | |

Production Experience

Сценарий: Парсинг CSV (10M строк):

// ПЛОХО — компиляция regex на каждой строке
for (String line : lines) {
    String[] fields = line.split(","); // 10M компиляций regex!
}

// ХОРОШО — pre-compiled Pattern
private static final Pattern COMMA = Pattern.compile(",");
for (String line : lines) {
    String[] fields = COMMA.split(line);
}

// ЛУЧШЕ — Fast Path (один символ, не meta)
for (String line : lines) {
    String[] fields = line.split(","); // Fast Path сработает!
}

Сценарий 2: Парсинг log-файла с regex-разделителем:

  • line.split("\\s\\|\\s") — не Fast Path, каждый вызов компилирует regex
  • Fix: private static final Pattern SEP = Pattern.compile("\\s\\|\\s");
  • Результат: -80% CPU на парсинг

Monitoring

// JMH бенчмарк
@Benchmark
public String[] testSplit() {
    return input.split(",");
}

@Benchmark
public String[] testPrecompiled() {
    return COMMA.split(input);
}

// Profile regex compilation
java -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions ...

Best Practices для Highload

  • Для односимвольных разделителей (не meta): split(",") — Fast Path
  • Для regex-разделителей: pre-compiled Pattern.compile(regex).split(str)
  • В hot paths: рассмотрите ручную реализацию через indexOf() — минимум аллокаций
  • Для CSV/TSV: специализированные библиотеки (OpenCSV, Apache Commons CSV)
  • Для ultra-low-latency: zero-copy парсинг через CharSequence wrappers

🎯 Шпаргалка для интервью

Обязательно знать:

  • split(regex) разбивает строку по регулярному выражению, возвращает массив
  • Fast Path: для односимвольных не-meta разделителей — прямой поиск без regex engine
  • limit параметр: 0 — пустые в конце удаляются, < 0 — сохраняются, > 0 — максимум элементов
  • Метасимволы regex нужно экранировать: . $ | ( ) [ ] { } ^ ? * + \
  • Pattern.quote(".") — безопасный способ экранирования для split()
  • Компиляция regex на каждой итерации цикла — антипаттерн, используйте pre-compiled Pattern

Частые уточняющие вопросы:

  • Почему split(".") не работает? — Точка в regex = “любой символ”. Нужно split("\\.").
  • Что делает split(",", -1)? — Сохраняет пустые строки в конце. По умолчанию (limit=0) они удаляются.
  • Что такое Fast Path в split()? — Если разделитель — один не-meta символ, используется прямой поиск без Pattern/Matcher.
  • Как оптимизировать split() в цикле? — Pre-compiled Pattern: private static final Pattern COMMA = Pattern.compile(",").

Красные флаги (НЕ говорить):

  • ❌ “split() принимает обычную строку, а не regex” — принимает regex, точка сломает логику
  • ❌ “split() всегда сохраняет пустые строки” — по умолчанию (limit=0) удаляет trailing empty
  • ❌ “Можно компилировать regex в цикле без последствий” — 10M компиляций = секунды CPU
  • ❌ “split() — единственный способ разбить строку” — есть indexOf(), StringTokenizer, специализированные парсеры

Связанные темы:

  • [[16. В чём особенность метода replace() vs replaceAll()]]
  • [[8. Как компилятор Java оптимизирует конкатенацию строк]]
  • [[7. Что происходит при конкатенации строк через оператор +]]