web-dev-qa-db-ja.com

Java.util.Scannerを使用してSystem.inからのユーザー入力を正しく読み取り、それに基づいて動作する方法

これは、重複するターゲットとして使用できる正規の質問/回答であることを意味します。これらの要件は、毎日投稿される最も一般的な質問に基づいており、必要に応じて追加できます。これらはすべて、各シナリオに到達するために同じ基本的なコード構造を必要とし、一般に互いに依存しています。


スキャナーは"simple"を使用するクラスのようであり、ここで最初の間違いが行われます。それは単純ではなく、非常に微妙な方法で最小驚きの原則を壊すあらゆる種類の非自明な副作用と異常な動作があります。

したがって、これはこのクラスにはやり過ぎのように見えるかもしれませんが、タマネギの皮むきのエラーと問題はすべてsimpleですが、まとめると、それらは非常にcomplexです。効果。これが、Stack Overflowで毎日非常に多くの質問がある理由です。

スキャナーに関する一般的な質問:

ほとんどのScanner質問には、これらのうちの複数の試行が失敗したことが含まれます。

  1. 前の各入力の後も次の入力を自動的に待機できるようにしたいのですが。

  2. exitコマンドを検出し、そのコマンドが入力されたときにプログラムを終了する方法を知りたい。

  3. exitコマンドの複数のコマンドを大文字と小文字を区別しない方法で照合する方法を知りたい。

  4. 組み込みのプリミティブだけでなく、正規表現のパターンにも一致できるようにしたいと考えています。たとえば、日付(_2014/10/18_)のように見えるものを照合するにはどうすればよいですか?

  5. 正規表現の一致では簡単に実装できない可能性のあるもの、たとえばURL(_http://google.com_)を一致させる方法を知りたいです。

動機:

Java=の世界では、Scannerは特別なケースであり、教師が新入生に使用する指示を与えてはならない非常に気難しいクラスです。ほとんどの場合、インストラクターはそれを正しく使用する方法を知っています。プロのプロダクションコードで使用することはほとんどないため、学生にとってのその価値は非常に疑わしいものです。

Scannerを使用すると、この質問と回答が言及する他のすべてのことを意味します。 Scannerだけでは決してありません。Scannerが間違っているほとんどすべての質問で共存問題であるScannerを使用してこれらの一般的な問題を解決する方法についてです。 next() vs nextLine() だけではありません。これは、クラスの実装の細かさの兆候であり、常に他の問題があります。 Scannerについて質問するコードの投稿。

答えは、Scannerが使用され、StackOverflowで質問された場合の99%の完全で慣用的な実装を示しています。

特に初心者コードでは。この答えが複雑すぎると思われる場合は、複雑さ、奇妙な点、明らかでない副作用、およびその動作の特殊性を説明する前に、Scannerを使用するように新入生に指示するインストラクターに文句を言います。

Scanner最小の驚きの原則 がいかに重要であり、メソッドとメソッド引数の命名において一貫した動作とセマンティクスが重要であるかについての素晴らしい教育の瞬間です。

学生への注意:

Scannerが実際に業務用/商用の基幹業務アプリで使用されることはおそらくないでしょう。実際のソフトウェアは、Scannerでコードを記述できるよりも、回復力と保守性が高くなければなりません。実際のソフトウェアでは、スタンドアロンの割り当てで指定されたadhoc入力形式ではなく、標準化されたファイル形式パーサーと文書化されたファイル形式を使用します。

23
user177800

慣用的な例:

以下は、_Java.util.Scanner_クラスを適切に使用して、_System.in_からユーザー入力をインタラクティブに正しく読み取る方法です(特にC、C++、およびその他の言語では、stdinと呼ばれることもあります)。 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 Pattern_または_Chain Of Responsibility_パターンに基づくディスパッチャーは、はるかに複雑な実際の問題に適しています。

エラー処理

一部のデータが正しくない可能性があるシナリオがないため、コードはException処理を必要としないように意図的に構造化されました。

.hasNext()および.hasNextXxx()

汎用の.hasNext()をテストしてイベントループを制御し、.hasNext()イディオムを使用して、if(.hasNextXxx())を適切に使用している人をめったに見かけない何も利用できず、例外処理コードがない場合に、intを要求することを心配することなく、コードをどのように処理するか。

.nextXXX() vs .nextLine()

これはみんなのコードを壊すものです。これは finicky detail であり、対処する必要はなく、非常に難読化されたバグがあるため、 Principal of Least Astonishment が壊れているため、推論することは困難です。

.nextXXX()メソッドは行末を消費しません。 .nextLine()が行います。

つまり、.nextLine()の直後に.nextXXX()を呼び出すと、行末だけが返されます。次の行を実際に取得するには、もう一度呼び出す必要があります。

このため、多くの人が.nextXXX()メソッドのみを使用するか、.nextLine()のみを使用し、両方を同時に使用しないことを支持しているため、この厄介な動作が邪魔になりません。個人的には、タイプセーフな方法の方が、手動でエラーをテストして解析してキャッチするよりもはるかに優れていると思います。

免疫力:

コードでは変更可能な変数が使用されていないことに注意してください。これは、実行方法を学ぶために重要です。これにより、ランタイムエラーと微妙なバグの4つの主要な原因が排除されます。

  1. いいえnullsNullPointerExceptionsの可能性がないことを意味します!

  2. 可変性がないということは、メソッドの引数の変更やその他の変更について心配する必要がないことを意味します。デバッグをステップ実行する場合、watchを使用して、どの変数がどの値に変更されているかを確認する必要はありません(変更されている場合)。これにより、ロジックを読んだときにロジックが100%確定的になります。

  3. 可変性がないということは、コードが自動的にスレッドセーフであることを意味します。

  4. 副作用はありません。何も変更できない場合、Edgeケースが予期せず何かを変更することによる微妙な副作用について心配する必要はありません。

独自のコードでfinalキーワードを適用する方法がわからない場合は、こちらをお読みください。

大規模なswitchまたは_if/elseif_ブロックの代わりにSetを使用する:

_Set<String>_を使用し、.contains()を使用してコマンドを分類する方法に注目してください。コードを肥大化させ、より重要な保守を行う巨大なswitchまたは_if/elseif_の代わりにコマンドを分類します。悪夢!新しいオーバーロードされたコマンドの追加は、コンストラクターの配列に新しいStringを追加するのと同じくらい簡単です。

これは、_i18n_および_i10n_と適切なResourceBundlesでも非常にうまく機能します。 _Map<Locale,Set<String>>_を使用すると、オーバーヘッドがほとんどなく、複数の言語をサポートできます。

@Nonnull

私はすべてのコードが explicitly が何かが_@Nonnull_または_@Nullable_であるかどうかを宣言する必要があると判断しました。 IDEは、潜在的なNullPointerExceptionの危険性について、およびチェックする必要がない場合に警告するのに役立ちます。

最も重要なことは、これらのメソッドパラメータがnullであってはならないという将来の読者への期待を文書化しています。

.close()の呼び出し

あなたがそれをする前に、これについて本当に考えてください。

sis.close()を呼び出した場合、_System.in_はどうなると思いますか?上記のリストのコメントを参照してください。

フォークしてプルリクエストを送信 してください。他の基本的な使用シナリオについて、この質問と回答を更新します。

19
user177800