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()
三兄弟。
ということで、おあとがよろしいようで。