7
1

More than 1 year has passed since last update.

トークンを理解すれば、勝手に入力スキップしちゃうScanner.nextXxx()三兄弟と仲良くなれる気がする

Last updated at Posted at 2022-12-14

Javaでコンソールアプリを作ると、ScannerクラスのnextXxx()を使って入力値を得るところがうまく制御できないことがある。なんか知らんけど、勝手に入力をスキップされて、処理が終わってしまったりして。

しれっと入力をスキップしてやり過ごすScannerインスタンスに対して、「お願い、怠惰なScannerクラスさん、もっと働いて!! 1」とか思ったり思わなかったりして……知らんけど!!

そんなくだらないこと言うのはこれくらいにして、さて本題へ。

今回は特に
Scanner.next();
Scanner.nextInt();
Scanner.nextLine();
の違いに関して、トークン2とバッファーというものに注目しながら、挙動をみていきます。

よくあるお悩み: nextInt()next()の直後に、nextLine()を呼ぶと思ったように動きません!

よく目にするのは、こんなコードの例。

private static void tryToNextLineExample() {
    Scanner sc = new Scanner(System.in);

    System.out.println("年齢を入力: ");
    int age = sc.nextInt();
    System.out.println(age + "歳ですね。");

    System.out.println("ついでに名前も入力: ");
    String name = sc.nextLine();
    System.out.println(name + "さん、はじめまして!");

    sc.close();
}

これの実行結果はこんな感じになる。

// 出力結果
年齢を入力: 
200                 // 200と入力後にエンター押下
200歳ですね。        // 結果を表示
ついでに名前も入力:   // って聞いてるのに、入力する暇なく...
さん、はじめまして!  // 完

2つ目の名前の入力がスキップされてしまう。
名前を尋ねておきながら!名前を入力するスキを与えてくれないのだ!!
一応、礼儀として名前は尋ねるけれども、実際には年齢だけしか回答を受け付けないなんて。失礼な奴なんだ、Scannerって奴は!(個人の感想です)。

ちなみに、nextInt()のところをnext()に(& ageの型をvarなどに)書き換えて実行しても、同じような事態が起こる。

// 上記一部抜粋&書き換え
System.out.println("年齢を入力: ");
var age = sc.next();
System.out.println(age + "歳ですね。");

対策

どうしてこんなことが起きるのかという話の前に、まずは簡単に対策を。
sc.nextInt()もしくはsc.next()を呼び出したあと、sc.nextLine()で名前を聞く前に、sc.nextLine()をかます。それだけ。

Scanner sc = new Scanner(System.in);

System.out.println("年齢を入力: ");
var age = sc.next();
System.out.println(age + "歳ですね。");
sc.nextLine(); // ここでかましてやる!!!

System.out.println("ついでに名前も入力: ");
String name = sc.nextLine();
System.out.println(name + "さん、はじめまして!");

sc.close();
// 出力結果
年齢を入力: 
200                // 200と入力してからエンター
200歳ですね。         
ついでに名前も入力:       
名無しの権兵衛    // 名前入力後、エンター
名無しの権兵衛さん、はじめまして!

めでたしめでたし。

理由

さて、ではなんでこんなことが起こるのか。それをちゃんと理解するために、まずはトークンをご紹介する。
ざっくりと言えば、nextInt()(&next())と、nextLine()はの違いは、返す値の違いだ。

nextInt()&next()は、トークンを返す。nextLine()は行を返す。3

じゃあ、トークンとは何か?

トークンと空欄(white space)

トークンは、ひとまとまりの単語/数字を表す。そのトークンの区切りは、空欄(white space)だ。この空欄、スペースだけでなく、\t(タブ)や改行\nも含まれる 4

Scanner.nextXxx() 空欄( )に出くわした時 tab(\t)に出くわした時 enter(\n)に出くわした時 型に関する制約
next() その直前までの入力値取得 (左に同じく) (左に同じく) 特になし
nextInt() その直前までの入力値取得後、Interger.parseInt() (左に同じく) (左に同じく) 入力値がIntegerにcastできないとInputMismatchExceptionを投げる
nextLine() 見過ごす=入力値に含める (左に同じく) その直前までの入力値取得 特になし

ここにあるように、nextLine()はトークンガン無視です。改行まで待ってから、全部バッサリ狩っていうワイルドな奴なんだな!

トークンとバッファー

またScannerを理解するには、バッファーというのも大切だ。バッファーは言わば、読み取った値の一時保管所。
なんと、目に見えない\nまで保管してくれる凄腕なのだ。けれどもこの\nまで保管してしまうというのが、厄介でもある。
あとはScannerクラスのメソッドも、呼び出されたらまずバッファーの残りを見に行く。もし何か残し物があれば、そちらを優先して読み込む。空だったら観念して、新しくユーザーからの入力を待つ=文字入力ができる状態になる。

さて。先ほどの、イケていない例を参考に、このバッファーの挙動を追っていこう。

// うまくいかないver.
Scanner sc = new Scanner(System.in);

System.out.println("年齢を入力: ");
int age = sc.nextInt();
// ユーザーの挙動:             "200"と押したあとに、エンターを押す
// => バッファーに保存された値: "200\n"

// 保存されたバッファーからnextInt()が"200"だけを取得、(内部でIntegerにcast後) ageに代入
// => バッファーに保存された値: "\n"

System.out.println(age + "歳ですね。");

// この時点でバッファーに保存されている値: "\n"
System.out.println("ついでに名前も入力: ");
String name = sc.nextLine();
// まずはバッファーを読み込み、残った文字列を発見するnextLine()
// "\n"を見つけた瞬間、その直前までの値("")を迷わずnameに代入

System.out.println(name + "さん、はじめまして!");

sc.close();
// うまくいくver.
Scanner sc = new Scanner(System.in);

System.out.println("年齢を入力: ");
int age = sc.nextInt();
// この挙動は上記と同様なので割愛

System.out.println(age + "歳ですね。");

// この時点でバッファーに保存されている値: "\n"
sc.nextLine();
// バッファーに残された"\n"を読み込んで、バッファーを空にする

// この時点でバッファーに保存されている値: (なし)
System.out.println("ついでに名前も入力: ");
String name = sc.nextLine();
// バッファーが空だったので、ユーザーからの入力を受け付ける
// エンター直前までの値を読み込み、nameに代入

System.out.println(name + "さん、はじめまして!");

sc.close();

トークンの区切り文字

トークンの区切りは、上記で触れた空欄ならなんでもOK。その結果、いけてないver.だと入力値によっては、こんなことも起こり得る。

年齢を入力:
200                // "200"と入力後、tabキー、エンターキーを順に押下
200歳ですね。      // この時点のバッファーの値: "\t\n"
ついでに名前も入力:
        さん、はじめまして! // バッファー内の"\n"までの文字列="\t"だけnameに代入

理解のための、極端な事例

極端な事例だと、こんなことも可能。

// 極端な事例
Scanner sc = new Scanner(System.in);

System.out.println("年齢 名前の順で入力し、最後にエンターを押してください(年齢と名前の間はスペースを入れてください!) 例:12 名無しの権兵衛");
var age = sc.next();
System.out.println(age + "歳ですね。");

String name = sc.nextLine();
System.out.println(name + "さん、はじめまして!");

sc.close();
// 極端な事例の実行結果
年齢 名前の順で入力し、最後にエンターを押してください(年齢と名前の間はスペースを入れてください
!) 例:12 名無しの権兵衛
200 名無しの権兵衛                 // ユーザーが"200 名無しの権兵衛"と入力
200歳ですね。                     // age = 200
 名無しの権兵衛さん、はじめまして! // name = " 名無しの権兵衛"

さて、本当に怠惰だったのは、一体誰だったのか。そう、こういう細かいことを飛ばして適当にコード書いてた私だ。ごめんねScannerクラスのnextXxx()三兄弟。

ということで、おあとがよろしいようで。

  1. 実際はもっと切羽詰まった感じになりますが、ほら、会社の名前で書いているものだから、それなりにオブラートに包んだつもり。

  2. ドキュメントとかだと出てくるけど、初心者向けの本だとあまり出てこない印象がある用語だなーと思っている(もしくは私がひどく流し読みしているか!?)。つまり、この単語の意味がさっぱりだと、ドキュメント読むにも詰まるんだよね。ということで、抑えておくといいのかなと思っている。

  3. Scanner class

  4. Chapter 11 – Input/Output and Exception Handling (p.17)

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1