W3docs

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, чтобы поведенческие различия были очевидны:

java— editable, runs on the server

Два вывода из выходных данных. В случае 1 split сообщает о пустой ячейке между green и blue ([red, green, , blue]); токенизатор схлопывает её ([red, green, blue]). В случае 2 оба варианта случайно дают одинаковые токены, но по разным причинам: токенизатор разбивает mix на каждой ; и на каждом пробеле независимо ("; " означает «любой из этих символов — разделитель»), тогда как split("; ") сопоставляет буквальную двухсимвольную последовательность "; ". Здесь они совпадают лишь потому, что разделители в mix — ровно ; . Измените входные данные на "k1=v1 ;k2=v2" — и результаты немедленно разойдутся. Какое бы поведение вы ни хотели получить, код должен явно это выражать — и с split это сделать значительно проще.

Что дальше

До сих пор мы разбивали строки на части. Но часто вам на самом деле нужно превратить строку в число, boolean или другой примитив — и в обратном направлении: примитивы обратно в строки. Этот цикл преобразований имеет собственный набор вспомогательных методов и подводных камней. Продолжите с главы Преобразования строк в Java.

Практика

Практика
Какое из следующих утверждений является **реальным поведенческим отличием** между `StringTokenizer` и `String.split`?
Какое из следующих утверждений является **реальным поведенческим отличием** между `StringTokenizer` и `String.split`?
Was this page helpful?