Java StringTokenizer
Разбивайте строки на токены с помощью устаревшего класса StringTokenizer и узнайте, когда лучше использовать String.split.
java.util.StringTokenizer — это оригинальный JDK-класс для «разбивки строки на части»: посимвольный токенизатор, присутствующий в платформе с версии 1.0. Сам Javadoc не рекомендует его для нового кода: «StringTokenizer — устаревший класс, сохранённый в целях совместимости, хотя его использование в новом коде не рекомендуется. Всем, кому нужна эта функциональность, рекомендуется использовать метод split класса String или пакет java.util.regex».
Это главное, и это верно — однако StringTokenizer по-прежнему встречается в реальных кодовых базах и занимает несколько честных ниш. В этой главе мы кратко рассмотрим его, а затем чётко скажем, когда стоит воспользоваться String.split или Scanner.
Базовый цикл
StringTokenizer — это Enumeration подстрок:
StringTokenizer st = new StringTokenizer("apple banana cherry");
while (st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
// apple
// banana
// cherryПо умолчанию разделителем служат пробельные символы — пробел, табуляция, перевод строки, возврат каретки, подача листа. Несколько соседних разделителей схлопываются: " a b " даёт ровно два токена.
Пользовательские разделители
Второй конструктор принимает строку, каждый символ которой трактуется как отдельный разделитель — а не как строка-разделитель целиком:
StringTokenizer st = new StringTokenizer("one,two;three|four", ",;|");
while (st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
// one
// two
// three
// fourЭто конструктивная особенность, которая нередко сбивает людей с толку. new StringTokenizer(input, ", ") трактует , и пробел каждый как разделитель, а не двухсимвольную последовательность ", ". Если вам нужны многосимвольные разделители, StringTokenizer вам уже не подходит.
Возврат самих разделителей
Трёхаргументный конструктор управляет тем, выдаются ли разделители как токены:
StringTokenizer st = new StringTokenizer("a+b*c", "+*", true);
while (st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
// a
// +
// b
// *
// cЭто единственная функциональность, которую String.split напрямую не воспроизводит: её пришлось бы реализовывать через Matcher и регулярное выражение. Для самого простого разбора выражений — такого, что не оправдывает подключение лексера — эта перегрузка всё ещё оправданна.
Подсчёт токенов заранее
countTokens() сообщает, сколько токенов осталось (то есть сколько было бы создано повторными вызовами nextToken()). При этом токены не потребляются.
StringTokenizer st = new StringTokenizer("a b c d");
int n = st.countTokens(); // 4
while (st.hasMoreTokens()) st.nextToken();
n = st.countTokens(); // 0Иногда это полезно при выделении выходного массива нужного размера — хотя с String.split вы просто вызвали бы .split(...).length.
Смена разделителя на ходу
Менее известная возможность: nextToken(String newDelims) сбрасывает набор разделителей для данного вызова и далее. После смены новый набор сохраняется для последующих вызовов hasMoreTokens()/nextToken(), пока вы его снова не измените.
StringTokenizer st = new StringTokenizer("key1=value1; key2=value2; key3=value3");
while (st.hasMoreTokens()) {
String pair = st.nextToken("; ").trim(); // tokens separated by ';' or space
System.out.println(pair);
}Для разового ситуативного разбора это может быть удобно. Для любого поддерживаемого кода это запутано — читатели не ожидают, что набор разделителей меняется внутри цикла.
Почему String.split обычно лучше
Причины предпочесть String.split (или Pattern.compile(...).split) в новом коде:
- Настоящие регулярные выражения в разделителях. Многосимвольные разделители, классы символов, альтернации — всё естественно.
StringTokenizerобрабатывает только односимвольные разделители. - Пустые токены видны.
"a,,b".split(",")возвращает["a", "", "b"].StringTokenizerмолча пропускает пустой токен. Для данных в формате CSV «второе поле было пустым» — это информация, которая обычно важна. - Возвращает массив. Удобно индексировать, удобно конвертировать в
List, удобно передавать в стрим. - Как правило, быстрее под JIT благодаря кешированию
Patternдля простых шаблонов разбивки. - Проще тестировать, проще читать. Цикл с токенизатором выглядит как 1996 год;
parts = csv.split(",")читается как намерение.
Когда всё же стоит его выбрать
Краткий список случаев, где StringTokenizer ещё оправдан:
- Потоковая обработка очень длинной строки, когда вы хотите потреблять токены по одному, не держа в памяти массив из всех них.
StringTokenizerне выделяет результат заранее;splitвыделяет. - Возврат разделителей как токенов для максимально простого токенизатора, без обращения к
Pattern/Matcher. - Поддержка старого кода, где остальной файл уже использует его, и согласованность — лучшее, что можно сделать для следующего читателя.
Во всех остальных случаях: используйте split.
Практический пример
Программа ниже токенизирует три входных строки тремя разными способами, рядом с эквивалентными вызовами split, чтобы поведенческие различия были очевидны:
Два вывода из выходных данных. В случае 1 split сообщает о пустой ячейке между green и blue ([red, green, , blue]); токенизатор схлопывает её ([red, green, blue]). В случае 2 оба варианта случайно дают одинаковые токены, но по разным причинам: токенизатор разбивает mix на каждой ; и на каждом пробеле независимо ("; " означает «любой из этих символов — разделитель»), тогда как split("; ") сопоставляет буквальную двухсимвольную последовательность "; ". Здесь они совпадают лишь потому, что разделители в mix — ровно ; . Измените входные данные на "k1=v1 ;k2=v2" — и результаты немедленно разойдутся. Какое бы поведение вы ни хотели получить, код должен явно это выражать — и с split это сделать значительно проще.
Что дальше
До сих пор мы разбивали строки на части. Но часто вам на самом деле нужно превратить строку в число, boolean или другой примитив — и в обратном направлении: примитивы обратно в строки. Этот цикл преобразований имеет собственный набор вспомогательных методов и подводных камней. Продолжите с главы Преобразования строк в Java.