Как использовать java.util.Scanner, чтобы правильно считывать вводимые пользователем данные System.in и действовать в соответствии с ними?
Это должен быть канонический вопрос / ответ, который можно использовать как дублирующий целевой объект. Эти требования основаны на наиболее распространенных вопросах, публикуемых каждый день, и могут быть добавлены по мере необходимости. Все они требуют одинаковой базовой структуры кода для доступа к каждому из сценариев, и обычно они зависят друг от друга.
Scanner кажется "простым" классом в использовании, и именно здесь совершается первая ошибка. Это непросто, у него есть всевозможные неочевидные побочные эффекты и отклоняющееся поведение, которые очень тонко нарушают принцип наименьшего удивления.
Итак, это может показаться излишеством для этого класса, но все ошибки и проблемы, связанные с очисткой лука, просты, но вместе взятые они очень сложны из-за их взаимодействия и побочных эффектов. Вот почему на Stack Overflow каждый день возникает так много вопросов по этому поводу.
Распространенные вопросы сканера:
Большинство Scanner
вопросов включают неудачные попытки выполнить более одной из этих задач.
Я хочу, чтобы моя программа также могла автоматически ожидать следующего ввода после каждого предыдущего ввода.
Я хочу знать, как обнаружить команду exit и завершить мою программу при вводе этой команды.
Я хочу знать, как сопоставить несколько команд для команды exit без учета регистра.
Я хочу иметь возможность сопоставлять шаблоны регулярных выражений, а также встроенные примитивы. Например, как сопоставить то, что выглядит как дата (
2014/10/18
)?Я хочу знать, как сопоставлять вещи, которые нелегко реализовать с помощью сопоставления регулярных выражений - например, URL (
http://google.com
).
Мотивация:
В мире Java, Scanner
это особый случай, это чрезвычайно сложный класс, преподаватели не должны давать новым студентам инструкции по использованию. В большинстве случаев преподаватели даже не знают, как правильно им пользоваться. Он практически никогда не используется в профессиональном производственном коде, поэтому его ценность для студентов крайне сомнительна.
Использование Scanner
подразумевает все остальные вещи, упомянутые в этом вопросе и ответе. Речь никогда не идет просто о Scanner
, речь идет о том, как решить эти распространенные проблемы с помощью Scanner
, которые всегда являются сопутствующими проблемами почти во всех вопросах, которые получаются Scanner
неправильно. Речь никогда не идет только о next()
vs nextLine()
, это всего лишь признак сложности реализации класса, всегда есть другие проблемы в коде, публикуемом в вопросах, касающихся Scanner
.
Ответ показывает полную идиоматическую реализацию в 99% случаев, когда Scanner
используется и о нем спрашивают в StackOverflow.
Особенно в коде для начинающих. Если вы считаете этот ответ слишком сложным, то пожалуйтесь преподавателям, которые советуют новым студентам использовать Scanner
, прежде чем объяснять тонкости, причуды, неочевидные побочные эффекты и особенности его поведения.
Scanner
это отличный обучающий момент о том, насколько важен принцип наименьшего удивления и почему согласованное поведение и семантика важны при именовании методов и аргументов метода.
Примечание для студентов:
Вы, вероятно, никогда на самом деле не увидите,
Scanner
используется в профессиональных / коммерческих бизнес-приложениях, потому что все, что он делает, делается лучше с помощью чего-то другого. Программное обеспечение реального мира должно быть более устойчивым и ремонтопригодным, чемScanner
позволяет вам писать код. Программное обеспечение реального мира использует стандартизированные анализаторы форматов файлов и документированные форматы файлов, а не специальные форматы ввода, которые вам даются в отдельных заданиях.
Переведено автоматически
Ответ 1
Идиоматический пример:
Ниже показано, как правильно использовать java.util.Scanner
класс для интерактивного чтения пользовательского ввода из System.in
правильно (иногда называемый stdin
, особенно в C, C ++ и других языках, а также в Unix и Linux). Он идиоматически демонстрирует наиболее распространенные вещи, которые требуется выполнить.
package com.stackoverflow.scanner;
import javax.annotation.Nonnull;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.regex.Pattern;
import static java.lang.String.format;
public class ScannerExample
{
private static final Set<String> EXIT_COMMANDS;
private static final Set<String> HELP_COMMANDS;
private static final Pattern DATE_PATTERN;
private static final String HELP_MESSAGE;
static
{
final SortedSet<String> ecmds = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
ecmds.addAll(Arrays.asList("exit", "done", "quit", "end", "fino"));
EXIT_COMMANDS = Collections.unmodifiableSortedSet(ecmds);
final SortedSet<String> hcmds = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
hcmds.addAll(Arrays.asList("help", "helpi", "?"));
HELP_COMMANDS = Collections.unmodifiableSet(hcmds);
DATE_PATTERN = Pattern.compile("\\d{4}([-\\/])\\d{2}\\1\\d{2}"); // http://regex101.com/r/xB8dR3/1
HELP_MESSAGE = format("Please enter some data or enter one of the following commands to exit %s", EXIT_COMMANDS);
}
/**
* Using exceptions to control execution flow is always bad.
* That is why this is encapsulated in a method, this is done this
* way specifically so as not to introduce any external libraries
* so that this is a completely self contained example.
* @param s possible url
* @return true if s represents a valid url, false otherwise
*/
private static boolean isValidURL(@Nonnull final String s)
{
try { new URL(s); return true; }
catch (final MalformedURLException e) { return false; }
}
private static void output(@Nonnull final String format, @Nonnull final Object... args)
{
System.out.println(format(format, args));
}
public static void main(final String[] args)
{
final Scanner sis = new Scanner(System.in);
output(HELP_MESSAGE);
while (sis.hasNext())
{
if (sis.hasNextInt())
{
final int next = sis.nextInt();
output("You entered an Integer = %d", next);
}
else if (sis.hasNextLong())
{
final long next = sis.nextLong();
output("You entered a Long = %d", next);
}
else if (sis.hasNextDouble())
{
final double next = sis.nextDouble();
output("You entered a Double = %f", next);
}
else if (sis.hasNext("\\d+"))
{
final BigInteger next = sis.nextBigInteger();
output("You entered a BigInteger = %s", next);
}
else if (sis.hasNextBoolean())
{
final boolean next = sis.nextBoolean();
output("You entered a Boolean representation = %s", next);
}
else if (sis.hasNext(DATE_PATTERN))
{
final String next = sis.next(DATE_PATTERN);
output("You entered a Date representation = %s", next);
}
else // unclassified
{
final String next = sis.next();
if (isValidURL(next))
{
output("You entered a valid URL = %s", next);
}
else
{
if (EXIT_COMMANDS.contains(next))
{
output("Exit command %s issued, exiting!", next);
break;
}
else if (HELP_COMMANDS.contains(next)) { output(HELP_MESSAGE); }
else { output("You entered an unclassified String = %s", next); }
}
}
}
/*
This will close the underlying InputStream, in this case System.in, and free those resources.
WARNING: You will not be able to read from System.in anymore after you call .close().
If you wanted to use System.in for something else, then don't close the Scanner.
*/
sis.close();
System.exit(0);
}
}
Примечания:
Это может показаться большим количеством кода, но это иллюстрирует минимальные усилия, необходимые для правильного использования
Scanner
класса и отсутствия необходимости иметь дело с малозаметными ошибками и побочными эффектами, которые досаждают новичкам в программировании и этому ужасно реализованному классу под названиемjava.util.Scanner
. В нем предпринята попытка проиллюстрировать, как должен выглядеть и вести себя идиоматический Java-код.
Ниже приведены некоторые из вещей, о которых я думал, когда писал этот пример:
Версия JDK:
Я намеренно сохранил совместимость этого примера с JDK 6. Если какой-то сценарий действительно требует функции JDK 7/8, я или кто-то другой опубликую новый ответ с подробностями о том, как это изменить для этой версии JDK.
Большинство вопросов об этом классе поступают от студентов, и у них обычно есть ограничения на то, что они могут использовать для решения проблемы, поэтому я ограничил это, насколько мог, чтобы показать, как выполнять обычные действия без каких-либо других зависимостей. За 22 с лишним года, что я работаю с Java и консультирую большую часть этого времени, я никогда не сталкивался с профессиональным использованием этого класса в исходном коде из 10 миллионов строк, который я видел.
Команды обработки:
Здесь точно показано, как идиоматически читать команды от пользователя в интерактивном режиме и отправлять эти команды. Большинство вопросов о java.util.Scanner
относятся к как я могу заставить свою программу завершать работу при вводе некоторой определенной категории ввода. Это ясно показывает.
Наивный диспетчер
Логика отправки намеренно наивна, чтобы не усложнять решение для новых читателей. Диспетчер, основанный на шаблоне Strategy Pattern
or Chain Of Responsibility
, был бы более подходящим для реальных проблем, которые были бы намного сложнее.
Обработка ошибок
Код был намеренно структурирован так, чтобы не требовать Exception
обработки, потому что нет сценария, при котором некоторые данные могли бы быть неверными.
.hasNext()
и .hasNextXxx()
Я редко вижу, чтобы кто-нибудь использовал .hasNext()
должным образом, тестируя универсальный .hasNext()
для управления циклом событий, а затем используя if(.hasNextXxx())
идиому, позволяющую вам решать, как и что делать с вашим кодом, не беспокоясь о запросе int
, когда ничего не доступно, следовательно, нет кода обработки исключений.
.nextXXX()
против .nextLine()
Это то, что нарушает код каждого пользователя. Это тонкая деталь, с которой не следует иметь дело, и она содержит очень запутанную ошибку, о которой трудно рассуждать, поскольку она нарушает принцип наименьшего удивления
.nextXXX()
Методы не используют окончание строки. .nextLine()
использует.
Это означает, что вызов .nextLine()
сразу после .nextXXX()
просто вернет окончание строки. Вы должны вызвать его снова, чтобы фактически получить следующую строку.
Вот почему многие люди рекомендуют либо не использовать ничего, кроме .nextXXX()
методов, либо только .nextLine()
, но не оба одновременно, чтобы такое привередливое поведение не сбивало вас с толку. Лично я считаю, что типобезопасные методы намного лучше, чем необходимость затем тестировать, разбирать и отлавливать ошибки вручную.
Неизменность:
Обратите внимание, что в коде не используются изменяемые переменные, важно научиться это делать, это устраняет четыре наиболее основных источника ошибок во время выполнения и малозаметных багов.
Нет
nulls
означает невозможностьNullPointerExceptions
!Отсутствие изменчивости означает, что вам не нужно беспокоиться об изменении аргументов метода или о чем-либо еще. При пошаговой отладке вам никогда не придется использовать
watch
, чтобы увидеть, какие переменные меняются на какие значения, если они меняются. Это делает логику на 100% детерминированной при чтении.Отсутствие изменяемости означает, что ваш код автоматически потокобезопасен.
Никаких побочных эффектов. Если ничего не может измениться, то вам не нужно беспокоиться о каком-то незаметном побочном эффекте неожиданного изменения чего-либо в крайнем случае!
Прочтите это, если вы не понимаете, как применить final
ключевое слово в вашем собственном коде.
Использование Set вместо массивных блоков switch
или if/elseif
:
Обратите внимание, как я использую Set<String>
и use .contains()
для классификации команд вместо массивного switch
или if/elseif
уродства, которое раздуло бы ваш код и, что более важно, превратило бы обслуживание в кошмар! Добавить новую перегруженную команду так же просто, как добавить новую String
в массив в конструкторе.
Это также будет очень хорошо работать с i18n
и i10n
и надлежащим ResourceBundles
. A Map<Locale,Set<String>>
позволит вам поддерживать несколько языков с очень небольшими накладными расходами!
@Nonnull
Я решил, что весь мой код должен явно объявлять, является ли что-то @Nonnull
или @Nullable
. Это позволяет вашей IDE help предупреждать вас о потенциальных NullPointerException
опасностях и о том, когда вам не нужно проверять.
Самое главное, что это документирует ожидание будущих читателей, что ни один из этих параметров метода не должен быть null
.
Вызов .close()
Действительно подумайте об этом, прежде чем делать это.
Как вы думаете, что произойдет, System.in
если вы вызовете sis.close()
? Смотрите Комментарии в списке выше.