繁体   English   中英

如何使用 java.util.Scanner 从 System.in 正确读取用户输入并对其采取行动?

[英]How to use java.util.Scanner to correctly read user input from System.in and act on it?

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


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

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

扫描仪常见问题:

大多数Scanner问题都包括尝试不止一项的失败。

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

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

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

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

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

动机:

在 Java 世界中, Scanner是一个特例,它是一个非常挑剔的类,老师不应该给新学生使用说明。 在大多数情况下,教师甚至不知道如何正确使用它。 它几乎没有用于专业生产代码,因此它对学生的价值非常值得怀疑。

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

答案显示了 99% 使用Scanner并在 StackOverflow 上被询问的情况的完整、惯用的实现。

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

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

学生须知:

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

惯用语示例:

以下是如何正确使用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 并在大部分时间进行咨询,我从未在我见过的数百万行源代码中遇到过此类的专业使用。

处理命令:

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

天真的调度员

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

错误处理

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

.hasNext().hasNextXxx()

我很少看到有人正确使用.hasNext() ,通过测试通用.hasNext()来控制事件循环,然后使用if(.hasNextXxx())习惯用法让您决定如何以及如何处理您的代码无需担心在没有可用的情况下要求int ,因此没有异常处理代码。

.nextXXX().nextLine()

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

.nextXXX()方法不消耗行尾。 .nextLine()确实如此。

这意味着,调用.nextLine()后立即.nextXXX()将只返回结束行。 您必须再次调用它才能真正获得下一行。

这就是为什么许多人主张要么只使用.nextXXX()方法,要么只使用.nextLine()但不要同时使用这两种方法,这样这种挑剔的行为就不会绊倒你。 我个人认为类型安全的方法比手动测试、解析和捕获错误要好得多。

不变性:

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

  1. 没有nulls意味着不可能出现NullPointerExceptions

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

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

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

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

使用 Set 而不是大量的switchif/elseif块:

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

这也适用于i18ni10n以及适当的ResourceBundles Map<Locale,Set<String>>可以让您以很少的开销获得多语言支持!

@非空

我决定我的所有代码都应该明确声明是@Nonnull还是@Nullable 它让您的 IDE 帮助您警告潜在的NullPointerException危险以及何时不必检查。

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

调用 .close()

在你做这件事之前真的想一想。

如果您调用sis.close()您认为System.in会发生什么? 请参阅上面列表中的注释。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM