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

21 java system.in java.util.scanner

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


扫描仪看起来像一个"简单"的类,这是第一个错误的地方.它并不简单,它具有各种非明显的副作用和异常行为,以非常微妙的方式打破了最小惊讶原则.

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

常见扫描仪问题:

大多数Scanner问题包括尝试不止一次这些事情.

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

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

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

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

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

动机:

在Java世界中,Scanner是一个特殊情况,它是一个非常挑剔的课程,教师不应该给新学生使用说明.在大多数情况下,教师甚至不知道如何正确使用它.如果在专业的生产规范中使用它几乎没有,所以它对学生的价值是非常值得怀疑的.

使用Scanner意味着这个问题和答案提到的所有其他事情.Scanner关于如何解决这些常见问题并不仅仅是因为它Scanner几乎在所有Scanner出错的问题中始终存在共病问题.它永远不仅仅是关于next()vsnextLine(),这只是该类实现的挑剔的一个症状,在问题询问中的代码发布中总会存在其他问题Scanner.

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

特别是在初学者代码中.如果您认为这个答案过于复杂,那么Scanner请先告诉新学生使用的教师,然后再解释其复杂性,怪癖,非明显副作用及其行为的特点.

Scanner关于最小惊讶原则的重要性以及为什么一致的行为和语义在命名方法和方法论证中很重要,这是一个很好的教学时刻.

给学生注意:

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

小智 18

惯用例:

以下是如何正确使用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);
    }
}
Run Code Online (Sandbox Code Playgroud)

笔记:

这可能看起来像很多代码,但它说明了Scanner正确使用类所需的最小努力,而不必处理困扰那些新编程和这个非常实现的类的微妙错误和副作用 java.util.Scanner.它试图说明惯用的Java代码应该是什么样子和行为.

下面是我写这个例子时我想到的一些事情:

JDK版本:

我故意保持这个示例与JDK 6兼容.如果某些场景确实需要JDK 7/8的一个功能我或其他人会发布一个新的答案,详细说明如何为该版本JDK修改它.

关于这门课程的大多数问题都来自学生,他们通常会限制他们可以用来解决问题的方法,因此我尽可能地限制这一点,以显示如何在没有任何其他依赖关系的情况下完成常见的事情.在22年多的时间里,我一直在使用Java并且大部分时间都在咨询我从未在我看过的数百万行源代码中遇到过这个类的专业用法.

处理命令:

这显示了如何以交互方式从用户惯用地读取命令并分派这些命令.大多数问题java.util.Scanner是当我输入一些特定的输入类别时如何让我的程序退出.这清楚地表明了这一点

天真的调度员

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

错误处理

代码是故意构造的,不需要Exception处理,因为没有一些数据可能不正确的情况.

.hasNext() 和 .hasNextXxx()

我很少看到有人使用.hasNext()正确的,通过测试通用.hasNext()来控制事件循环,然后使用if(.hasNextXxx())成语让你决定如何以及如何继续你的代码,而不必担心要求int什么时候没有,所以没有异常处理代码.

.nextXXX() VS .nextLine()

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

该.nextXXX()方法不消耗行的结局..nextLine()确实.

这意味着.nextLine()之后立即调用.nextXXX()将返回行结束.你必须再次调用它来实际获得下一行.

这就是为什么许多人主张要么只使用.nextXXX()方法,要么只使用.nextLine()但不能同时使用两者,以便这种挑剔的行为不会让你失望.就个人而言,我认为类型安全方法比必须手动测试和解析并捕获错误要好得多.

不可变性:

请注意,代码中没有使用可变变量,这对于学习如何操作非常重要,它消除了四个最主要的运行时错误和微妙的错误.

  1. 没有nulls意味着没有可能 NullPointerExceptions!

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

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

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

如果您不了解如何final在自己的代码中应用关键字,请阅读此内容.

使用Set代替大量switch或if/elseif块:

请注意我如何使用a Set<String>并使用它.contains()来对命令进行分类,而不是使代码膨胀的大量switch或if/elseif怪异,更重要的是使维护成为一场噩梦!添加新的重载命令就像String在构造函数中向数组添加新内容一样简单.

这也适用于i18n和i10n适当的ResourceBundles.A Map<Locale,Set<String>>会让你拥有多种语言支持而且开销很小!

@Nonnull

我已经决定,我所有的代码应该明确宣布,如果事情是@Nonnull或@Nullable.它可以让您的IDE帮助警告您潜在的NullPointerException危险以及何时无需检查.

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

调用.close()

在你做之前真的想一想这个.

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

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

  • 我认为这个答案中的信噪比太低了.它应该是关于Scanner的,但有一些东西,比如immutability,sets vs switch,还有很多其他的东西.当你四处寻找可以用3行代码回答的问题作为复制时,你什么都不做,只会让他们感到困惑.与NRE规范相比,NRE需要深度.扫描仪没有. (16认同)