Как работает метод 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).