Почему не вылетает 2 исключения подряд?

Уважаемые, вопрос. Причём сразу скажу, что если Вы не шарите за нюансы работы методов класса Scanner и связанных с ними исключений - смело проходите дальше.

Тестируемый код:

public static void main(String[] args) {
    Scanner console = new Scanner(System.in);

    System.out.print("Введите x: ");
    int x = console.nextInt(); // "\n".

    while (true) {
        try {
            System.out.print("Введите y: ");
            int y = console.nextInt();
            break;
        } catch (Exception e) {
            console.nextLine();
        }

    }
}

Пример в онлайн.

Мы присвоили переменной x какое-нибудь число, а дальше начинает работать цикл, который не прервётся до тех пор, пока пользователь не введёт какое-либо целое число. Допустим он пишет какую-либо чушь. Перехватываем исключение ввода не int'a, очищаем сканнер console командой cosnole.nextLine(); и едем дальше снова и снова. Но вот не задача, перед самым первым витком цикла мы же уже считали число и присвоили его переменной x. В самом же сканнере незримо остаётся символ переноса строки "\n" после работы int x = console.nextInt(); Далее, допустим, пользователь пишет любое слово и в сканере уже следующая картина: \nслово\n.

Вылетает исключение;
перехватываем;
используем console.nextLine(); для очистки сканнера; но напомню, что в сканнере \nслово\n и данный метод должен споткнуться о первый \n и дальше передать на новый виток цикла слово\n;
далее опять перехват исключения и уже 2-е срабатывание console.nextLine(); очистит сканнер полностью, дойдя до последнего \n;
и таким образом, фраза "Введите y: " должна запринтится дважды, однако этого не происходит.
Почему?! Куда исчезает символ переноса строки после первого перехвата исключения у int y = console.nextInt();
Я знаю, что в штатном режиме при втором по счёту вводе числа, если в начале в console было: \n
мы ввели число;
в console: \nчисло\n
число считывает, первый символ \n также отлетает и в console остаётся: \n
Но если речь об исключении, то
в console: \n
вводим слово;
в console: \nслово\n;
вылетает исключение и, что же, получается, что оно скипает первый \n? И в console остаётся слово\n? Такая механика работы? Иначе, я это никак не могу объяснить! Получается, что исключение у метода console.nextInt(); при любых раскладах скипает первый символ \n, если таковой там изначально имелся. Но я в этом не уверен, потому что не знаю механики работы этого исключения. Всё же, я новичок, но новичок дотошный. Заранее respect тому, кто сможет объяснить!
И да, тесты должны быть одного вида, чтобы расшарить эту проблему:
1. какое-либо число
2. какое-либо слово


Ответы (3 шт):

Автор решения: Олексій Моренець

В свое время тоже долго разбирался в работе сканнера. И вот этот простой код с объяснением поставил все на свои места. Может, и вам поможет:

import java.util.Scanner;

public class ScannerExample {

    public static void main(String[] args) {
        /*
         * http://mastefanov.com/wp-content/uploads/2014/02/programmingJava.
         * lec3_.4Scanner.pdf
         */
        
        double doubleFoo;
        int integerBar;
        float floatQuiz;
        long longBaz;
        String token;
        String line;
        
        try (Scanner stdInScanner = new Scanner(System.in)) {
            doubleFoo = stdInScanner.nextDouble();
            integerBar = stdInScanner.nextInt();
            floatQuiz = stdInScanner.nextFloat();
            longBaz = stdInScanner.nextLong();
            token = stdInScanner.next();
            line = stdInScanner.nextLine();
        }
        
        System.out.printf("double=%f\nint=%d\nfloat=%f\nlong=%d\ntoken=%s\nline=%s\n", doubleFoo, integerBar, floatQuiz,
                longBaz, token, line);
        System.out.println("over");
    }

}

/*
 * Работа программы: 1,1 2 3 4 5 double=1,100000 int=2 float=3,000000 long=4
 * token=5 line= over Более детально работу метода printf() мы рассмотрим в
 * следующем параграфе, в общем смысл таков – первым параметром задается
 * форматирующая строка, содержащая специальные группы символов (начинающиеся со
 * знака %), которые в результате заменяются соответствующим значением из списка
 * параметров. Пользовательский ввод обозначен зеленым цветом, вывод программы –
 * черным. В случае с вещественными и целыми числами все выглядит логично.
 * Пользователь вводит число, после чего нажимает enter. Введённое им значение
 * попадает в поток System.in и считывается сканнером. В каждой строке
 * присутствует только одно число. При этом с помощью метода nextFloat() (и
 * nextDouble()) мы можете считать не только вещественные числа, но и целые (но
 * в результате они все равно будут представлены как вещественные). Таким
 * образом, мы успешно считываем 4 числа и записываем их в переменные,
 * соответственно doubleFoo, integerBar, floatQuiz, longBaz. Однако дальше
 * проявляется особенность метода nextLine(). После методов, считывающих числа,
 * идут вызовы next() и nextLine(): String token = stdInScanner.next(); String
 * line = stdInScanner.nextLine(); Казалось бы, после ввода пользователем числа
 * 5 (которое логично считывается методом next() и записывается в переменную
 * token), программа должна ожидать ввода строки – но этого не происходит, она
 * завершает свою работу, а переменная line в итоге не содержит никакого
 * значения. Давайте рассмотрим, что попадает в поток, когда пользователь вводит
 * 5 и нажимает enter. Во входном потоке оказывается последовательность бит,
 * которая соответствует символу ‘5’ и символу «конец строки». Конец строки – по
 * умолчанию является разделителем токенов, поэтому все, что было до него, было
 * считано методом next(), и маркер, который показывает текущее положение в
 * потоке, устанавливается после 5, но перед «концом строки». Затем вызывается
 * метод nextLine(), который успешно обнаруживает оставшуюся группу символов,
 * соответствующих «концу строки» и успешно считывает его. Но поскольку никаких
 * символов между маркером и концом строки нет, то в буфер ничего не заносится,
 * и следовательно, в результате методом ничего не возвращается и в переменную
 * line ничего не записывается. Пара слов, относительно группы символов «конец
 * строки». По стандарту, любое совместимое с Юникодом приложение должно
 * воспринимать как перевод строки каждый из нижеследующих символов: LF
 * (U+000A): англ. line feed — подача строки; CR (U+000D): англ. carriage return
 * — возврат каретки; CR + LF – рассматривается как один перевод строки, а не 2
 * NEL (U+0085): англ. next line — переход на следующую строку; LS (U+2028):
 * англ. line separator — разделитель строк; PS (U+2029): англ. paragraph
 * separator — разделитель абзацев. В стандартных текстовых файлах, использующих
 * кодировку ASCII в зависимости от операционной системы, в качестве разделителя
 * строк могут использоваться сомволы: LF – Unix, FreeBSD, MacOS… CR – (старые
 * версии MacOS) CR + LF – MSDOS, Windows. Рассмотрим еще один пример
 * пользовательского ввода предыдущей программы: 1,1 2 3 4 5 678 9
 * double=1,100000 int=2 float=3,000000 long=4 token=5 line= 678 9 over
 * По-прежнему, символы, вводимые пользователем помечены зеленым цветом. Этот
 * пример показывает несколько нюансов: 1) По-умолчанию, «лишние» пробелы при
 * поиске токенов во входном потоке с помощью метода класса Scanner
 * игнорируются. 2) Если в предыдущем случае все методы считывания блокировали
 * работу программы, пока пользователь не введет что-либо, то сейчас блокировка
 * осуществляется только методом nextDouble() и next(). 3) После считывания
 * какого-либо токена, остальные биты из потока не удаляются и могут быть
 * считаны последующими вызовами методов чтения. В данном случае, метод
 * nextDouble() блокирует выполнение программы. После того как пользователь ввел
 * строку «1,1 2 3 4» метод считал токен 1,1 и поместил маркер чтения сразу
 * после него. Следующий метод считал все пробелы, затем 2 и поместил маркер
 * сразу за символом 2. Затем в программе подряд вызывается еще 2 метода,
 * которые считывают, соответственно 3 и 4 не блокируя работу программы в
 * ожидании ввода. 4) Последняя строка показывает, что маркер чтения
 * устанавливается методами nextX() сразу после считанного токена (и следующие
 * за ним разделители считываются уже следующим вызовом nextX()). next() считал
 * 5, а nextLine() вернул строку из символов, между 5 и концом строки (включая
 * пробел между 5 и 678).
 */
→ Ссылка
Автор решения: Maxgmer

Очень хороший вопрос! Я знаю, как работает Scanner, но все равно пришлось покопаться, чтобы однозначно ответить на этот вопрос.

Расскажу порядок работы сканнера при вызове scanner.next() и аналогов для примитивов (nextDouble(), nextInt()), чтобы помочь вам разобраться.

  1. Scanner берет ввод и делит его на так называемые токены с помощью делимитеров. Считайте токен просто кусочком строки. Делимитер - паттерн, который может быть каким угодно, например, (\\s), то есть пробел (вы не задаете делимитер, поэтому у вас делимитер по умолчанию, о котором скажу далее).

  2. Scanner идет по токенам, пока токены не закончатся, либо пока не будет найден подходящий токен (например, число, в случае с nextInt()), либо пока парсинг токена не вызовет исключение. (Но, по моему предположению после многих попыток тестирования, первое исключение игнорируется, если оно вызвано токеном с предыдущего ввода).

  3. Когда выполнился проход по токенам (пункт 2), все последующие токены и делимитеры (если есть) остаются в сканнере, а пройденные удаляются.

Вы не задаете свой делимитер, поэтому используется делимитер по умолчанию, который можно получить так: System.out.println(console.delimiter().pattern());.

На выводе мы получим \p{javaWhitespace}+. Я был удивлен этому ответу, т.к. он меня поставил в тупик, я не знал, что такое javaWhiteSpace. Погугля, я нашел ответ:

There are four other white space characters in Java, the horizontal tab, the form feed, the carriage return, and the linefeed.

То есть, как видим, одним и делимитеров является перенос строки по умолчанию.


Теперь поясню, как работает nextLine(), чтобы понять отличие. Данный метод просто достает первый токен ввода сканнера (даже если токен пустой), а делимитером является не делимитер сканнера, а только перенос строки.


Теперь перейдем к примеру, чтобы объяснить работу сканнера получше:

Когда вы вводите x (к примеру, 5), у вас в сканнере появляется 5\n. Сканнер видит один делимитер и 2 токена: '5' и ''. Он начинает их парсить, первый токен подходит под Integer паттерн, поэтому nextInt() возвращает 5. В сканнере остается один делимитер \n и один токен ''.

Далее ваша программа просит y. Прежде чем просить ввод ваш ввод, Scanner посмотрит, что у него есть внутри из System.in:

  1. \n - делимитер.
  2. '' - токен. Дальше Scanner парсит токены ('' в нашем случае) и если не находит ничего интересного, не бросает исключение, а просит ввод, блокируя программу (сканнер не бросит эксепшен до запроса ввода, только если уже после ввода не будет ничего, что являлось бы Integer, тогда уже бросит эксепшен).

И так, Scanner ничего не нашел (\n и '' в нашем случае) и попросил ввод.

Давайте сделаем необычный ввод, чтобы отразить полноценно работу сканнера и вы поняли принцип. Введем '5 blablabla'.

Теперь у нас в сканнер ситуация такая:

  1. 5 - токен с текущего ввода.
  2. ' ' - делимитер с текущего ввода.
  3. blablabla - токен с текущего ввода.
  4. \n - делимитер с текущего ввода.

Scanner смотрит первый токен, видит там Integer, берет его и оставляет остальное у себя, программа не крашится, тк токен 5 валиден, а blablabla - уже совсем другой токен.


Вот так работает Scanner. Подытожим, почему Выбрать y: показывается только один раз. Это так потому, что nextInt() и подобные методы смотрять не на один токен, а на сколько угодно токенов до конца или до exception. А nextLine() берет только первый токен до разрыва строки.

→ Ссылка
Автор решения: Stanislav Volodarskiy

Уже есть хороший ответ со всеми подробностями. Не буду повторяться.

Скажу только что Scanner поддерживает две тактики работы с входным потоком: чтение по токенам (грубо говоря по словам) и по строкам. Все методы next, nextInt, nextLong и т.п. работают с токенами. А метод nextLine работает со строками.

Теперь к делу. Если вы хотите сделать интерактивный ввод удобный для пользователя-человека, работайте со строками. Считывайте строку целиком, затем преобразуйте в значение нужного типа. В случае ошибки со сканнером ничего делать не надо, никакого мусора во входном потоке нет:

import java.util.Scanner;

public class ScannerLines {
    private static int readInt(Scanner s, String prompt) {
        while (true) {
            System.out.print(prompt);
            String line = s.nextLine();
            try {
                return Integer.parseInt(line);
            } catch (NumberFormatException e) {
                System.err.println("Error: wrong integer");
            }
        }
    }

    public static void main(String... args) {
        Scanner s = new Scanner(System.in);
        int i = 0;
        while (true) {
            int v = readInt(s, "[" + i + "] Enter integer > ");
            System.out.println("[" + i + "] The integer is " + v + '\n');
            ++i;
        }
    }
}
$ java ScannerLines 
[0] Enter integer > 12
[0] The integer is 12

[1] Enter integer > 23f
Error: wrong integer
[1] Enter integer > 23 34
Error: wrong integer
[1] Enter integer > 23
[1] The integer is 23

[2] Enter integer > 34
[2] The integer is 34

Работа с токенами менее удобна. Пользователь может вводить числа вперед и это никак не поправить. В случае ошибки пропускайте ошибочный токен (метод next), а не всю строку (метод nextLine):

import java.util.InputMismatchException;
import java.util.Scanner;

public class ScannerTokens {
    private static int readInt(Scanner s, String prompt) {
        while (true) {
            System.out.print(prompt);
            try {
                return s.nextInt();
            } catch (InputMismatchException e) {
                System.err.println("Error: wrong integer");
                s.next();
            }
        }
    }

    public static void main(String... args) {
        Scanner s = new Scanner(System.in);
        int i = 0;
        while (true) {
            int v = readInt(s, "[" + i + "] Enter integer > ");
            System.out.println("[" + i + "] The integer is " + v + '\n');
            ++i;
        }
    }
}
$ java ScannerTokens 
[0] Enter integer > 12
[0] The integer is 12

[1] Enter integer > 23f
Error: wrong integer
[1] Enter integer > 23 34
[1] The integer is 23

[2] Enter integer > [2] The integer is 34

Повторюсь, пользователь может ввести несколько чисел на одной строке, у нас нет хорошего метода остановить его.

Пишете интерактивный ввод - работайте со строками. Пишете ввод из потока - работайте с токенами. И я бы не стал делать тут обработку ошибок с переспрашиванием, роботу она ни к чему.

Хотите совместить оба подхода? Подумайте ещё раз. Вы или получите странный интерактивный ввод или будете игнорировать ошибки при вводе из потока.

→ Ссылка