Питання 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. Що відбувається при конкатенації рядків через оператор +]]