Как использовать java.util.Scanner, чтобы правильно прочитать пользовательский ввод из System.in и действовать на него?

Это должен быть канонический вопрос / ответ, который можно использовать как дублирующую цель. Эти требования основаны на самых распространенных вопросах, публикуемых каждый день, и могут добавляться по мере необходимости. Все они требуют одинаковой базовой структуры кода для доступа к каждому из сценариев и, как правило, зависят друг от друга.


Сканер выглядит как "простой" класс для использования, и именно здесь совершается первая ошибка. Это не просто, у него есть все виды неочевидных побочных эффектов и отклонений в поведении, которые очень тонко нарушают Принцип Наименьшего Изумления.

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

Общие вопросы сканера:

Наиболее Scanner вопросы включают неудачные попытки более чем одной из этих вещей.

  1. Я хочу, чтобы моя программа автоматически ожидала следующего ввода после каждого предыдущего ввода.

  2. Я хочу знать, как определить команду выхода и завершить программу, когда эта команда введена.

  3. Я хочу знать, как сопоставить несколько команд для команды выхода без учета регистра.

  4. Я хочу иметь возможность сопоставлять шаблоны регулярных выражений, а также встроенные примитивы. Например, как сопоставить то, что кажется датой (2014/10/18)?

  5. Я хочу знать, как сопоставлять вещи, которые не могут быть легко реализованы с сопоставлением регулярных выражений, например, URL (http://google.com).

Мотивация:

В мире Java, Scanner Это особый случай, это очень привередливый класс, когда учителя не должны давать новым ученикам инструкции по использованию. В большинстве случаев инструкторы даже не знают, как правильно его использовать. Он вряд ли когда-либо используется в профессиональном производственном коде, поэтому его ценность для студентов крайне сомнительна.

С помощью Scanner подразумевает все остальные вещи, упомянутые в этом вопросе и ответе. Это никогда не только Scanner речь идет о том, как решить эти общие проблемы с Scanner которые всегда сопутствуют болезненным проблемам почти во всех вопросах, которые получают Scanner неправильно. Это никогда не только next() противnextLine(), это всего лишь признак тонкости реализации класса, всегда есть другие проблемы при размещении кода в вопросах о Scanner,

Ответ показывает полную идиоматическую реализацию в 99% случаев, когда Scanner используется и спрашивается о Stackru.

Особенно в коде для начинающих. Если вы считаете, что этот ответ слишком сложный, то пожаловаться на инструкторов, которые говорят новым ученикам использовать Scanner Прежде чем объяснять тонкости, причуды, неочевидные побочные эффекты и особенности его поведения.

Scanner Это прекрасный обучающий момент о том, как важен Принцип наименьшего удивления и почему согласованное поведение и семантика важны в именовании методов и аргументов методов.

Примечание для студентов:

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

1 ответ

Решение

Идиоматический пример:

Ниже описано, как правильно использовать java.util.Scanner класс для интерактивного чтения пользовательского ввода из System.in правильно (иногда упоминается как stdin особенно в C, C++ и других языках, а также в Unix и Linux). Это идиоматически демонстрирует наиболее распространенные вещи, которые просят сделать.

package com.stackru.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 и консультировал большую часть времени, я никогда не сталкивался с профессиональным использованием этого класса в десятках миллионов строк исходного кода, который я видел.

Обработка команд:

Это показывает, как именно идиоматически читать команды от пользователя в интерактивном режиме и отправлять эти команды. Большинство вопросов о java.util.Scanner из того, как я могу заставить свою программу выйти, когда я вхожу в какую-то определенную категорию ввода. Это показывает это ясно.

Наивный Диспетчер

Логика рассылки намеренно наивна, чтобы не усложнять решение для новых читателей. Диспетчер на основе Strategy Pattern или же Chain Of Responsibility шаблон будет более подходящим для реальных проблем, которые были бы гораздо более сложными.

Обработка ошибок

Код был преднамеренно структурирован, чтобы не требовать Exception обработка, потому что нет сценария, где некоторые данные могут быть неверными.

.hasNext() а также .hasNextXxx()

Я редко вижу, чтобы кто-нибудь использовал .hasNext() правильно, путем тестирования на общий .hasNext() управлять циклом событий, а затем с помощью if(.hasNextXxx()) идиома позволяет вам решить, как и что делать с вашим кодом, не беспокоясь о том, чтобы попросить int когда ни один не доступен, таким образом, никакой код обработки исключений.

.nextXXX() против .nextLine()

Это то, что нарушает код каждого. Это привередливая деталь, с которой не нужно разбираться, и в ней есть очень запутанная ошибка, о которой трудно рассуждать, потому что она нарушает Принцип Наименьшего Удивления

.nextXXX() методы не потребляют окончания строки. .nextLine() делает.

Это означает, что вызов .nextLine() незамедлительно после .nextXXX() просто вернет конец строки. Вы должны позвонить еще раз, чтобы получить следующую строку.

Вот почему многие люди выступают либо не используют ничего, кроме .nextXXX() методы или только .nextLine() но не то и другое одновременно, так что это привередливое поведение не сбивает вас с толку. Лично я считаю, что типобезопасные методы намного лучше, чем тестирование, анализ и обнаружение ошибок вручную.

Immutablity:

Обратите внимание, что в коде не используются изменяемые переменные, это важно, чтобы узнать, как это сделать, это устраняет четыре основных источника ошибок времени выполнения и тонких ошибок.

  1. нет nulls означает отсутствие возможности NullPointerExceptions!

  2. Отсутствие изменчивости означает, что вам не нужно беспокоиться об изменении аргументов метода или что-либо еще. Когда вы проходите отладку, вам никогда не придется использовать watch чтобы увидеть, какие переменные изменяются на какие значения, если они меняются. Это делает логику 100% детерминированной, когда вы читаете ее.

  3. Отсутствие изменчивости означает, что ваш код автоматически поточно-ориентирован.

  4. Нет побочных эффектов. Если ничто не может измениться, вам не нужно беспокоиться о каком-то тонком побочном эффекте какого-то крайнего случая, который неожиданно что-то изменит!

Прочтите это, если вы не понимаете, как применять final Ключевое слово в вашем собственном коде.

Использование набора вместо массивного switch или же if/elseif блоки:

Обратите внимание, как я использую Set<String> и использовать .contains() классифицировать команды вместо массивных switch или же if/elseif чудовищность, которая раздувает ваш код и, что еще важнее, делает обслуживание кошмаром! Добавить новую перегруженную команду так же просто, как добавить новую String в массив в конструкторе.

Это также будет очень хорошо работать с i18n а также i10n и собственно ResourceBundles, Map<Locale,Set<String>> позволит вам иметь многоязычную поддержку с очень небольшими накладными расходами!

@Nonnull

Я решил, что весь мой код должен явно объявить, если что-то @Nonnull или же @Nullable, Это позволяет вашей IDE помочь предупредить вас о возможном NullPointerException опасности и когда вам не нужно проверять.

Наиболее важно, что он документирует ожидание будущих читателей, что ни один из этих параметров метода не должен быть null,

Вызов.close()

Действительно подумай об этом, прежде чем сделать это.

Как ты думаешь, что произойдет System.in если бы ты позвонил sis.close()? Смотрите комментарии в листинге выше.

Пожалуйста, разветвите и отправьте запросы на включение, и я обновлю этот вопрос и ответ для других основных сценариев использования.

Другие вопросы по тегам