W3docs

Класс Scanner в Java

Разбор примитивов и строк из текстового ввода в Java с помощью класса Scanner — nextInt, nextLine, useDelimiter.

BufferedReader.readLine() из главы о буферизованных потоках — это правильный инструмент, когда входные данные разбиты на строки и вам нужна каждая строка как String. Scanner — это правильный инструмент, когда входные данные представляют собой поток токенов — целых чисел, чисел с плавающей точкой, слов, разделённых пробелами, или полей, разделённых регулярным выражением по вашему выбору. Это парсер, входящий в состав JDK.

Scanner — также класс, который большинство вводных учебников по Java используют для чтения с клавиатуры. new Scanner(System.in) — и у вас есть работающая интерактивная программа в двух строках. Это удобство сопряжено с одним хорошо известным подводным камнем — ловушкой nextInt/nextLine — которой и посвящена бо́льшая часть этой главы.

Что разбирает Scanner

Методы чтения токенов в паре с предикатами hasNext:

boolean hasNext();        String  next();           // a whitespace-delimited token
boolean hasNextInt();     int     nextInt();        // a token parsed as int
boolean hasNextLong();    long    nextLong();
boolean hasNextDouble();  double  nextDouble();
boolean hasNextBoolean(); boolean nextBoolean();
boolean hasNextLine();    String  nextLine();       // the rest of the current line

Контракт одинаков для всех типизированных методов: hasNextX() проверяет, можно ли следующий токен разобрать как X, не потребляя его; nextX() потребляет его. Несоответствие (nextInt(), когда токен — "hello") выбрасывает InputMismatchException. Конец потока выбрасывает NoSuchElementException.

Токен по умолчанию — это максимальная последовательность непробельных символов. Шаблон разделителя — это то, что Pattern.UNICODE_CHARACTER_CLASS считает пробельным символом: пробелы, табуляции, символы новой строки и подобные. Его можно изменить с помощью useDelimiter(...).

Конструкторы

new Scanner(InputStream source);                     // typical: System.in
new Scanner(InputStream source, Charset charset);    // explicit charset (preferred for files)
new Scanner(Path source, Charset charset);           // open a file by path
new Scanner(String source);                          // parse a literal String — great for tests
new Scanner(Readable source);                        // wrap any Readable (Reader, CharBuffer, ...)

То же правило, что и для остального java.io/java.nio: всегда передавайте явную кодировку при чтении байтов. Конструкторы без кодировки используют кодировку платформы по умолчанию.

try (Scanner s = new Scanner(path, StandardCharsets.UTF_8)) {
  while (s.hasNextInt()) {
    process(s.nextInt());
  }
}

Закрытие Scanner закрывает базовый поток. Не закрывайте Scanner, обёртывающий System.in — закрытие закроет System.in, и любые последующие операции чтения в той же JVM завершатся ошибкой.

Ловушка nextInt / nextLine

Самый задаваемый вопрос по Java на Stack Overflow.

Scanner s = new Scanner(System.in);
System.out.print("age: ");  int age = s.nextInt();
System.out.print("name: "); String name = s.nextLine();

Введите 30, нажмите Enter, затем Alice, нажмите Enter. Ожидается: age=30, name=Alice. Фактически: age=30, name="".

Причина: nextInt() читает цифры 30 и останавливается. В буфере ввода остаётся завершающий символ \n. Следующий nextLine() читает всё до следующего символа новой строки — который находится прямо там, немедленно — и возвращает пустую строку, не дав пользователю возможности что-либо ввести.

Исправление — одно из двух:

int age = s.nextInt(); s.nextLine();                 // explicit "skip to end of line"
String name = s.nextLine();

или, более надёжно, разбирайте всю строку самостоятельно:

int age = Integer.parseInt(s.nextLine().trim());     // always reads the full line
String name = s.nextLine();

Второй вариант — тот, к которому я прибегаю в реальном коде. Смешивание методов чтения токенов (nextInt, nextDouble, next) с построчным чтением (nextLine) — это рецепт ошибок на единицу смещения; выберите один подход и придерживайтесь его. Либо разбирайте строку за строкой с помощью nextLine, либо разбирайте токен за токеном с помощью next* и вызывайте nextLine только для явного пропуска остатка текущей строки.

hasNext как условие цикла

Типичная форма каждого цикла со Scanner:

while (s.hasNextInt()) {                             // predicate, no exception
  int n = s.nextInt();                               // consume
  process(n);
}

hasNextInt() возвращает false в конце потока и когда следующий токен не является целым числом — таким образом, цикл завершается чисто как на EOF, так и на нечисловом токене (что часто правильно, например когда завершающий нижний колонтитул нечисловой). Если вместо этого вы хотите явного сбоя, используйте hasNext() и дайте nextInt() выбросить InputMismatchException при несоответствии:

while (s.hasNext()) {
  int n = s.nextInt();                               // throws if the token isn't an int
  process(n);
}

Одна и та же проверка конца потока, разное поведение при некорректных токенах.

Пользовательские разделители

Разделитель по умолчанию — пробельные символы. Для CSV-подобного ввода его можно изменить:

s.useDelimiter(",|\\R");                             // comma or any line break

\\R — это Java-регулярное выражение для «любой последовательности новой строки» (\n, \r\n, \r, плюс Unicode-разделители строк). Объединённый шаблон разбивает по запятым и символам новой строки, поэтому 1,2,3\n4,5,6 даёт шесть токенов.

Тем не менее: для настоящего CSV используйте CSV-библиотеку. Scanner не обрабатывает поля в кавычках, экранированные запятые или встроенные символы новой строки. Для простых случаев — список чисел, конфигурация, разделённая пробелами — он идеален.

Подводный камень с локалью

nextDouble() разбирает значение с использованием десятичного разделителя локали по умолчанию. На немецкой JVM 3.14 вызовет ошибку (3,14 — немецкая форма). На американской JVM 3,14 вызовет ошибку.

Для машиночитаемых входных данных принудительно задайте локаль парсера:

s.useLocale(Locale.ROOT);                            // dot as decimal separator, no grouping
double x = s.nextDouble();                           // now parses "3.14"

Locale.ROOT — «нейтральная» локаль — соглашение для разбора файлов данных, не предназначенных для людей. Забыть это — наиболее частая причина того, что CSV-ридер работает в разработке и ломается в CI: машина разработчика и машина CI имеют разные локали по умолчанию.

Scanner vs BufferedReader

ScannerBufferedReader
Читаеттокены (типизированные)строки (String)
Скоростьмедленно (регулярное выражение на каждый токен)быстро
Удобствовысокое (nextInt и др.)низкое (разбор самостоятельно)
Подходит длянебольших входных данных, интерактивных запросов, тестовбольших файлов, обработки логов, горячих циклов

Практическое правило: если ввод от человека и вам нужны типы — используйте Scanner. Если ввод из файла и вам нужны строки — используйте BufferedReader. Для входных данных уровня соревновательного программирования (миллионы токенов) BufferedReader + StringTokenizer работает на порядок быстрее, чем Scanner.

Практический пример: разбор небольшого текстового формата

Программа ниже разбирает небольшой текстовый файл, разделённый пробелами, с тремя полями в строке — id name score — с использованием Scanner. Она демонстрирует цикл hasNextInt(), исправление локали для nextDouble(), ловушку nextInt/nextLine и её решение, а также useDelimiter для CSV-подобной альтернативы.

java— editable, runs on the server

Что стоит вынести из запуска:

  • Первое чтение разобрало три записи трёх разных типов в три строки кода. Токенный API действительно удобен, когда входные данные имеют вид токенов — без регулярных выражений, без String.split, без ручного Integer.parseInt. Это и есть случай применения Scanner.
  • useLocale(Locale.ROOT) — строка, которая сделала 97.5 разбираемым. Без неё парсер использует локаль JVM по умолчанию; на машине, где это немецкая локаль, 97.5 выбросило бы InputMismatchException. Для машиночитаемых входных данных всегда фиксируйте локаль.
  • Разделение на «с ошибкой»/«исправленный» для ловушки вывело name='', а затем name='Alice'. Ошибка была реальной — nextInt() оставил \n в буфере — а построчное исправление (Integer.parseInt(s.nextLine().trim())) было наиболее чистым способом избежать смешивания двух стилей чтения. Выберите стиль и придерживайтесь его.
  • Блок useDelimiter("," + "|" + "\\R") разобрал строки, разделённые запятыми, тем же кодом чтения токенов, только с другим разделителем. Тот же нюанс применяется, что и в тексте: это работает для чистого CSV и ломается на реальном CSV с полями в кавычках. Используйте настоящую CSV-библиотеку для всего, что пришло из Excel.
  • Нижний колонтитул смешанного ввода (-- end --) показал, почему hasNextInt() — правильное условие цикла: он вернул false при первом нецелочисленном токене, и цикл завершился чисто. Переключение на hasNext() позволило бы циклу продолжаться до тех пор, пока nextInt() не выбросил бы исключение — оба варианта полезны в зависимости от того, означает ли нецелочисленный токен «мы закончили» или «входные данные некорректны».

Что дальше

PrintWriter (предыдущая глава) и Scanner — это символьно-ориентированные классы ввода/вывода, которые использует большинство вводного Java-кода. Следующая глава, Java PrintStream, охватывает байтово-ориентированного родственника PrintWriter — и объясняет, почему System.out и System.err являются PrintStream, а не PrintWriter.

Практика

Практика
На JVM с немецкой локалью по умолчанию вы вызываете `scanner.nextDouble()` для разбора '3.14' из конфигурационного файла. Что произойдёт и каково исправление?
На JVM с немецкой локалью по умолчанию вы вызываете `scanner.nextDouble()` для разбора '3.14' из конфигурационного файла. Что произойдёт и каково исправление?
Was this page helpful?