如何使用java.util.Scanner正确读取 System.in 的用户输入并对其执行操作?

2022-09-01 07:51:49

这是一个规范的问题/答案,可以用作重复的目标。这些要求基于每天发布的最常见问题,可以根据需要添加。它们都需要相同的基本代码结构才能到达每个方案,并且它们通常相互依赖。


Scanner似乎是一个“简单”的类,这是第一个错误。它并不简单,它有各种不明显的副作用和异常行为,以非常微妙的方式打破了最小惊讶原则

所以对于这门课来说,这似乎有些矫枉过正,但剥洋葱的错误和问题都很简单,但加在一起,由于它们的相互作用和副作用,它们非常复杂。这就是为什么每天在Stack Overflow上有这么多关于它的问题。

常见的扫描仪问题:

大多数问题包括不止一件这些事情的失败尝试。Scanner

  1. 我希望我的程序能够在每次上一个输入之后自动等待下一个输入。

  2. 我想知道如何检测exit命令并在输入该命令时结束我的程序。

  3. 我想知道如何以不区分大小写的方式匹配退出命令的多个命令。

  4. 我希望能够匹配正则表达式模式以及内置基元。例如,如何匹配看似日期( )?2014/10/18

  5. 我想知道如何匹配那些可能不容易用正则表达式匹配实现的东西 - 例如,URL ( )。http://google.com

动机:

在Java世界中,这是一个特例,它是一个非常挑剔的课程,教师不应该给新生使用指导。在大多数情况下,教师甚至不知道如何正确使用它。它几乎从未在专业生产代码中使用过,因此它对学生的价值非常值得怀疑。Scanner

使用意味着这个问题和答案提到的所有其他事情。它从来不只是关于如何解决这些常见问题,这些问题在几乎所有出错的问题上总是共同病态的问题。它从来不只是关于next() vs nextLine(),这只是类实现的挑剔的症状,在问题中总是有其他问题发布。ScannerScannerScannerScannerScanner

答案显示了在StackOverflow上使用和询问的99%案例的完整,惯用的实现。Scanner

特别是在初学者代码中。如果你认为这个答案太复杂了,那么向老师抱怨,告诉新学生在解释其行为的复杂性,怪癖,不明显的副作用和特殊性之前使用。Scanner

Scanner这是一个伟大的教学时刻,关于最小惊讶原则有多重要,以及为什么一致的行为和语义在命名方法和方法参数中很重要。

学生须知:

您可能永远不会真正看到在专业/商业业务线应用程序中使用,因为它所做的一切都可以通过其他方法做得更好。现实世界的软件必须比编写代码更具弹性和可维护性。现实世界的软件使用标准化的文件格式解析器和记录的文件格式,而不是在独立分配中给出的临时输入格式。ScannerScanner


答案 1

惯用示例:

以下是如何正确使用该类以交互方式正确读取用户输入(有时称为,特别是在C,C++和其他语言以及Unix和Linux中)。它习惯性地展示了要求完成的最常见的事情。java.util.ScannerSystem.instdin

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);
    }
}

笔记:

这可能看起来像很多代码,但它说明了正确使用该类所需的最小工作量,而不必处理困扰那些刚开始编程的人的微妙错误和副作用,以及这个名为 的非常实现的类。它试图说明习语Java代码应该是什么样子和行为。Scannerjava.util.Scanner

以下是我在编写此示例时正在考虑的一些事项:

JDK 版本:

我故意让这个例子与JDK 6兼容。如果某些场景确实需要JDK 7/8的功能,我或其他人将发布一个新的答案,其中包含有关如何为该版本修改此版本JDK的详细信息。

关于这门课的大多数问题都来自学生,他们通常对他们可以用来解决问题的东西有限制,所以我尽可能地限制了这一点,以展示如何在没有任何其他依赖关系的情况下做常见的事情。在我使用Java和咨询的22年多时间里,我从未见过在我见过的1000万行源代码中专业使用这个类。

处理命令:

这准确地显示了如何以交互方式从用户那里读取命令并调度这些命令。大多数问题都是关于当我进入某些特定的输入类别时如何让我的程序退出。这清楚地表明了这一点。java.util.Scanner

天真的调度员

调度逻辑是故意天真的,以免使新读者的解决方案复杂化。基于 或 模式的调度程序将更适合于更复杂的现实世界问题。Strategy PatternChain Of Responsibility

错误处理

代码被故意构造为不需要处理,因为不存在某些数据可能不正确的情况。Exception

.hasNext().hasNextXxx()

我很少看到任何人正确使用,通过测试泛型来控制事件循环,然后使用这个成语让你决定如何以及什么继续你的代码,而不必担心在没有可用时询问一个,因此没有异常处理代码。.hasNext().hasNext()if(.hasNextXxx())int

.nextXXX().nextLine()

这是打破每个人代码的东西。这是一个挑剔的细节,不应该被处理,并且有一个非常模糊的错误,很难推理,因为它打破了最小惊讶的原则

这些方法不使用行尾。 确实如此。.nextXXX().nextLine()

这意味着紧接着调用将只返回行结束。您必须再次调用它才能实际获得下一行。.nextLine().nextXXX()

這就是為什麼許多人提倡要么只使用方法,要么只使用但不能同時使用兩者,這樣這種挑剔的行為就不會讓你騷擾。就个人而言,我认为类型安全方法比手动测试,解析和捕获错误要好得多。.nextXXX().nextLine()

不受欢迎:

请注意,代码中没有使用可变变量,这对于学习如何做很重要,它消除了四个最主要的运行时错误和细微错误来源。

  1. 没有意味着没有可能!nullsNullPointerExceptions

  2. 没有可变性意味着您不必担心方法参数更改或其他任何更改。当您单步调试时,您永远不必使用来查看哪些变量正在更改为哪些值(如果它们正在更改)。这使得逻辑在读取时具有 100% 的确定性。watch

  3. 没有可变性意味着您的代码是自动线程安全的。

  4. 无副作用。如果没有什么可以改变的,你不必担心一些边缘情况意外改变某些东西的微妙副作用!

如果您不了解如何在自己的代码中应用 final 关键字,请阅读本文。

使用集合而不是大块或块:switchif/elseif

请注意,我如何使用 a 和 来对命令进行分类,而不是一个庞大的或怪物,这会膨胀你的代码,更重要的是使维护成为一场噩梦!添加新的重载命令就像在构造函数中向数组添加新命令一样简单。Set<String>.contains()switchif/elseifString

这也将与 和 和 适当的 一起工作。A可以让您以很少的开销获得多语言支持!i18ni10nResourceBundlesMap<Locale,Set<String>>

@Nonnull

我已经决定,我的所有代码都应该显式声明如果某些东西是 或 。它允许您的IDE帮助您警告潜在的危险,以及您不必检查。@Nonnull@NullableNullPointerException

最重要的是,它记录了对未来读者的期望,即这些方法参数都不应该是 。null

调用 .close()

在你做之前,真的要考虑一下这个。

如果您打电话,您认为会发生什么?请参阅上面列表中的评论。System.insis.close()

分叉并发送拉取请求,我将更新此问题,并回答其他基本使用场景。


答案 2

推荐