Як працює метод split()
Метод split() розбиває вихідний рядок на масив підрядків на основі заданого роздільника.
🟢 Junior Level
Метод split() розбиває вихідний рядок на масив підрядків на основі заданого роздільника.
Головна особливість методу полягає в тому, що аргумент-роздільник сприймається не як звичайний текст, а як регулярний вираз (Regular Expression / Regex).
Суть у 30 секундах
- Екранування метасимволів: Символи, що мають спеціальне значення в регулярних виразах (крапка
., вертикальна риска|, плюс+, зірочка*, знак питання?тощо), необхідно обов’язково екранувати подвійним слешем\\або методомPattern.quote(). Наприклад,str.split("\\."). - Видалення порожніх рядків у кінці (Trailing Empty Strings): За замовчуванням метод
split(regex)відкидає порожні рядки наприкінці результату. Щоб зберегти їх, необхідно використовувати перевантаження з від’ємним лімітом:split(regex, -1).
// 1. Стандартне розділення
String data = "apple,banana,orange";
String[] fruits = data.split(",");
// Результат: ["apple", "banana", "orange"]
// 2. Пастка з крапкою:
String ip = "192.168.1.1";
String[] bad = ip.split("."); // ПОМИЛКА: поверне порожній масив! (крапка = будь-який символ)
String[] good = ip.split("\\."); // ПРАВИЛЬНО: ["192", "168", "1", "1"]
// 3. Пастка з порожніми «хвостами»:
String csv = "user,123,,,";
String[] defaultSplit = csv.split(","); // ["user", "123"] (порожні хвости відсічені!)
String[] fullSplit = csv.split(",", -1); // ["user", "123", "", "", ""] (усе збережено)
🟡 Middle Level
Перевантаження методу та параметр limit
У класі String оголошено два перевантаження методу:
public String[] split(String regex) {
return split(regex, 0);
}
public String[] split(String regex, int limit)
Поведінка методу повністю визначається параметром limit:
| Значення limit | Максимум елементів | Обробка кінцевих порожніх рядків | Приклад для "a,b,,,".split(",", limit) |
| :— | :— | :— | :— |
| limit = 0 (за замовчуванням) | Без обмежень | ❌ Видаляються всі порожні рядки наприкінці | ["a", "b"] |
| limit < 0 (наприклад, -1) | Без обмежень | ✅ Зберігаються всі порожні рядки наприкінці | ["a", "b", "", "", ""] |
| limit > 0 (наприклад, 2) | Не більше ніж limit | ✅ Зберігаються (залишок рядка цілком в останньому елементі) | ["a", "b,,,"] |
Швидкий шлях (Fast-Path Optimization)
Багато розробників вважають, що виклик split() завжди компілює важкий регулярний вираз через java.util.regex.Pattern. У HotSpot JVM це не так.
Якщо роздільник відповідає критеріям Fast-Path, JVM виконує простий посимвольний пошук без створення regex-рушія:
- Довжина рядка-роздільника становить рівно 1 символ, і цей символ не є метасимволом regex (
.$|()[{^?*+\\). - Довжина рядка-роздільника становить рівно 2 символи, перший символ —
\\, а другий — не є літерою ASCII або цифрою (наприклад,split("\\.")абоsplit("\\|")).
str.split(","); // FAST-PATH: швидкий пошук indexOf, без regex-рушія
str.split("\\."); // FAST-PATH: швидкий пошук крапки, без regex-рушія
str.split("\\s+"); // SLOW-PATH: компіляція Pattern, обхід скінченного автомата
Типові помилки
split("|")без екранування: Символ|у regex означає логічне «АБО». У результаті рядок розіб’ється на окремі символи з порожніми елементами між ними. Правильно:str.split("\\|").- Компіляція regex у циклі:
// ПОГАНО: 1 000 000 разів компілює один і той самий Pattern for (String line : lines) { String[] parts = line.split("\\s+"); } // ДОБРЕ: передкомпільований Pattern private static final Pattern SPACES = Pattern.compile("\\s+"); for (String line : lines) { String[] parts = SPACES.split(line); }
🔴 Senior Level
Аналіз вихідного коду OpenJDK HotSpot
Усередині методу java.lang.String.split(String regex, int limit) перевірка Fast-Path реалізована на бітових масках:
public String[] split(String regex, int limit) {
char ch = 0;
if (((regex.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))
{
// FAST-PATH
int off = 0;
int next = 0;
boolean limited = limit > 0;
ArrayList<String> list = new ArrayList<>();
while ((next = indexOf(ch, off)) != -1) {
if (!limited || list.size() < limit - 1) {
list.add(substring(off, next));
off = next + 1;
} else {
// Досягнуто межі limit: зберігаємо весь хвіст рядка
int last = length();
list.add(substring(off, last));
off = last;
break;
}
}
// ... відсікання порожніх рядків при limit == 0
return list.toArray(new String[resultSize]);
}
// SLOW-PATH: компіляція Pattern
return Pattern.compile(regex).split(this, limit);
}
Накладні витрати пам’яті та алокацій
Навіть у режимі Fast-Path стандартний метод split() має приховані витрати:
- Створюється допоміжний
ArrayList<String>. - На кожному кроці викликається
substring(off, next), що створює новий об’єктStringта масив байтівbyte[]у купі. - Фінальний виклик
list.toArray(new String[...])алокує результуючий масив посилань. У режимі Slow-Path додатково виділяються:- Екземпляр
Patternіз деревом вузлів абстрактного синтаксичного дерева (AST). - Екземпляр
Matcherіз внутрішніми масивами груп.
- Екземпляр
Високопродуктивні альтернативи (Zero-Allocation / Low-Overhead)
У Highload-системах для парсингу текстових протоколів (CSV, FIX, HTTP-заголовки) String.split() зазвичай не використовується:
Pattern.splitAsStream()(Java 8+): Дозволяє ліниво обробляти елементи черезStream<String>без попереднього формування повного масиву в пам’яті.- Guava
Splitter:Splitter.on(',').trimResults().omitEmptyStrings().split(line); - Ручний парсинг через
indexOfіз буфером повторного використання: Дозволяє обробляти мільйони рядків на секунду з нульовим навантаженням на збирач сміття (Zero Allocation).
4 Tricky Questions
1. Що повернуть вирази ",".split(",") та ",".split(",", -1)?
Відповідь:
",".split(",")поверне порожній масив нульової довжиниnew String[0]. Пояснення: Кома ділить рядок на два порожні підрядки:["", ""]. Оскільки параметрlimit = 0за замовчуванням видаляє всі порожні рядки наприкінці, обидва порожні рядки відсікаються, і результуючий масив стає порожнім.",".split(",", -1)поверне масив із двох порожніх рядків:["", ""]. Пояснення: Параметрlimit = -1забороняє видалення кінцевих порожніх рядків, тому обидві порожні частини зберігаються.
2. Що поверне виклик "".split(",") на абсолютно порожньому рядку?
Відповідь:
Виклик "".split(",") поверне масив з одного елемента, що містить порожній рядок: [""] (довжина масиву 1).
Пояснення: Рядок "" не містить роздільника ,. Згідно з контрактом методу, якщо роздільник не знайдено, вихідний рядок цілком поміщається як єдиний елемент результуючого масиву.
3. Як за допомогою параметра limit безпечно розпарсити рядок конфігурації "key=value=extra" рівно на ключ і значення?
Відповідь:
Необхідно передати limit = 2:
String property = "key=value=extra";
String[] parts = property.split("=", 2);
// parts[0] -> "key"
// parts[1] -> "value=extra"
При limit = 2 метод виконає рівно одне розділення за першим знайденим знаком =, а весь залишок рядка (навіть якщо він містить інші знаки =) недоторканим запише в другий елемент масиву.
4. Чому str.split("\\s+") виконується в десятки разів повільніше, ніж str.split(" ") при багаторазовому виклику?
Відповідь:
str.split(" ")потрапляє у Fast-Path: пробіл є односимвольним роздільником без метасимволів regex. JVM обходить масив байтів простим лінійним пошукомindexOf(' '), не задіюючи рушій регулярних виразів.str.split("\\s+")містить квантифікатор+та метасимвол\s, тому Fast-Path не спрацьовує. При кожному виклику методу викликаєтьсяPattern.compile("\\s+"), який парсить рядок регулярного виразу, будує дерево станів скінченного автомата, виділяє пам’ять підMatcherта виконує зіставлення з відкатами (backtracking).
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь
«Метод
split(regex, limit)розбиває рядок за регулярним виразом. За замовчуванням (limit = 0) усі порожні рядки наприкінці результату відкидаються; щоб їх зберегти, передаютьlimit = -1. Якщо роздільник — один символ (не метасимвол regex) або екранований символ ("\\."), HotSpot використовує Fast-Path на базі швидкогоindexOf, не створюючи рушійPattern. Складні regex компілюються черезPattern.compile(), тому в циклах їх обов’язково потрібно кешувати вstatic final Pattern».
Ключові тези для Senior-інтерв’ю
- Параметр
limit:0— відсікати порожні хвости;< 0— зберігати все;> 0— повернути не більше ніжlimitелементів (залишок в останньому). - Fast-Path критерії: Рядок довжиною 1 символ (не meta) або 2 символи (
\+ не-буквено-цифровий ASCII). - Метасимволи:
.$|()[{^?*+\\вимагають екранування\\абоPattern.quote(). - Edge Cases:
"".split(",")$\to$[""];",".split(",")$\to$[];",".split(",", -1)$\to$["", ""]. - Highload-оптимізація: Заміна на передкомпільований
Pattern,Pattern.splitAsStream(), GuavaSplitterабо ручнийindexOfдля Zero-Allocation.
Червоні прапорці (Чого говорити категорично не можна)
- ❌ Стверджувати: «
split()приймає звичайний рядок, а не регулярний вираз». - ❌ Думати, що
split(".")розділить рядок за крапками (крапка збігається з усіма символами). - ❌ Вважати, що
split()за замовчуванням зберігає порожні рядки наприкінці. - ❌ Заявляти, що
split()завжди компілюєPattern(забуваючи про Fast-Path).