Почему не вылетает 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).
*/
Очень хороший вопрос! Я знаю, как работает Scanner, но все равно пришлось покопаться, чтобы однозначно ответить на этот вопрос.
Расскажу порядок работы сканнера при вызове scanner.next() и аналогов для примитивов (nextDouble(), nextInt()), чтобы помочь вам разобраться.
Scannerберет ввод и делит его на так называемые токены с помощью делимитеров. Считайте токен просто кусочком строки. Делимитер - паттерн, который может быть каким угодно, например,(\\s), то есть пробел (вы не задаете делимитер, поэтому у вас делимитер по умолчанию, о котором скажу далее).Scannerидет по токенам, пока токены не закончатся, либо пока не будет найден подходящий токен (например, число, в случае сnextInt()), либо пока парсинг токена не вызовет исключение. (Но, по моему предположению после многих попыток тестирования, первое исключение игнорируется, если оно вызвано токеном с предыдущего ввода).Когда выполнился проход по токенам (пункт 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:
\n- делимитер.''- токен. ДальшеScannerпарсит токены (''в нашем случае) и если не находит ничего интересного, не бросает исключение, а просит ввод, блокируя программу (сканнер не бросит эксепшен до запроса ввода, только если уже после ввода не будет ничего, что являлось быInteger, тогда уже бросит эксепшен).
И так, Scanner ничего не нашел (\n и '' в нашем случае) и попросил ввод.
Давайте сделаем необычный ввод, чтобы отразить полноценно работу сканнера и вы поняли принцип. Введем '5 blablabla'.
Теперь у нас в сканнер ситуация такая:
5- токен с текущего ввода.' '- делимитер с текущего ввода.blablabla- токен с текущего ввода.\n- делимитер с текущего ввода.
Scanner смотрит первый токен, видит там Integer, берет его и оставляет остальное у себя, программа не крашится, тк токен 5 валиден, а blablabla - уже совсем другой токен.
Вот так работает Scanner.
Подытожим, почему Выбрать y: показывается только один раз. Это так потому, что nextInt() и подобные методы смотрять не на один токен, а на сколько угодно токенов до конца или до exception. А nextLine() берет только первый токен до разрыва строки.
Уже есть хороший ответ со всеми подробностями. Не буду повторяться.
Скажу только что 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
Повторюсь, пользователь может ввести несколько чисел на одной строке, у нас нет хорошего метода остановить его.
Пишете интерактивный ввод - работайте со строками. Пишете ввод из потока - работайте с токенами. И я бы не стал делать тут обработку ошибок с переспрашиванием, роботу она ни к чему.
Хотите совместить оба подхода? Подумайте ещё раз. Вы или получите странный интерактивный ввод или будете игнорировать ошибки при вводе из потока.