Класс 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
Scanner | BufferedReader | |
|---|---|---|
| Читает | токены (типизированные) | строки (String) |
| Скорость | медленно (регулярное выражение на каждый токен) | быстро |
| Удобство | высокое (nextInt и др.) | низкое (разбор самостоятельно) |
| Подходит для | небольших входных данных, интерактивных запросов, тестов | больших файлов, обработки логов, горячих циклов |
Практическое правило: если ввод от человека и вам нужны типы — используйте Scanner. Если ввод из файла и вам нужны строки — используйте BufferedReader. Для входных данных уровня соревновательного программирования (миллионы токенов) BufferedReader + StringTokenizer работает на порядок быстрее, чем Scanner.
Практический пример: разбор небольшого текстового формата
Программа ниже разбирает небольшой текстовый файл, разделённый пробелами, с тремя полями в строке — id name score — с использованием Scanner. Она демонстрирует цикл hasNextInt(), исправление локали для nextDouble(), ловушку nextInt/nextLine и её решение, а также useDelimiter для CSV-подобной альтернативы.
Что стоит вынести из запуска:
- Первое чтение разобрало три записи трёх разных типов в три строки кода. Токенный 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.