LoginSignup
110

More than 5 years have passed since last update.

JavaのScannerとか.nextLine()の挙動をよく分かってなかった話

Last updated at Posted at 2017-05-24

事の発端

Javaでこんな感じのコードを書く

Main1.java
import java.util.*;

/**
 * Created 2017/05/23.
 */

public class Main1 {
  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    ArrayList<String> ary = new ArrayList<String>();
    int num = sc.nextInt();
    for (int i = 0; i < num; i++) {
      String word = sc.nextLine();
      ary.add(word);
    }
    System.out.println(ary);
  }
}

プログラムコンテストとかでよくある
1:最初に数字の入力を受け取る
2:1で入力された回数分、さらに入力を受け取る
3:2の入力値を色々あーだーこーだする
っていう感じのプログラムが必要になったため、
回数入力→入力されたStringをArrayListに格納という形で書く
が、この実行結果が妙なことに

入力値
2
a
b(実際には入力できず)

実行結果
[, a]

謎の空白?が入ってしまう事態になったので、その原因やらを追究することに・・・・・・

まずは.nextLine()の仕様を把握することに

まずはリファレンスで仕様を把握しよう

https://docs.oracle.com/javase/jp/8/docs/api/java/util/Scanner.html#nextLine--

nextLine

public String nextLine()
スキャナを現在行の先に進めて、スキップした入力を返します。このメソッドは、最後の行区切り文字を除く、現在行の残りを返します。位置は、次の行の最初に設定されます。
このメソッドは行区切り文字の検索を入力内で続行するため、行区切り文字が存在しない場合、スキップする行を検索する入力をすべてバッファすることがあります。
戻り値:
スキップされた行
例外:
NoSuchElementException - 行が見つからなかった場合
IllegalStateException - このスキャナがクローズしている場合


うん、何言ってるかさっぱりだな
とりあえず色々と調べた結果
・.nextLine()は空白(空行)も読み込む
という事は分かった
しかし今度はどこから空白(空行)が発生しているのか、という疑問にぶち当たる

実際の挙動を一個一個確かながら調べる

実際に調べながらソースコードを書いて確かめることに(頭で悩むより手を動かす方が色々早い)
ソースコードのファイル名の順番がずれますが、これは色々試した結果だと思ってください
リファレンスだけじゃこんなん読み取れないよ・・・・・・

まずはSacannerと.nextLine()の動きを簡単に見る

Sacannerと.nextLine()の動き

Main5

Main5.java
import java.util.Scanner;

/**
 * Created 2017/05/25.
 * nextLineの挙動とか
 */
public class Main5 {
   public static void main(String[] args) {
      String word = "123 test";
      Scanner sc = new Scanner(word);
      String line = sc.nextLine();

      System.out.println(word);
      //実行結果:123 test
      System.out.println(line);
      //実行結果:123 test

   }
}
実行結果
123 test
123 test

これは非常に分かりやすい

Main6

次に複数のスキャンをかけてみます

Main6.java
import java.util.Scanner;

/**
 * Created 2017/05/25.
 */
public class Main6 {
  public static void main(String[] args){
    String word = "123 test";
    Scanner sc = new Scanner(word);
    int num     = sc.nextInt(); 
    String line = sc.nextLine();

    System.out.println(word);
    //実行結果:123test

    System.out.println(num);
    //実行結果:123

    System.out.println(line);
    //実行結果: test

  }
}

実行結果
123 test
123
 test

ここで注目したいのは、lineの出力結果になります
事前に.nextInt();をした後に、.nextLine();を行うと、数字の部分が既に読み込まれているため
空白+testしか残らないことが分かります

じゃあもし全部読み込んだ後に.nextLine()をしたら?
試してみよう!

Main7

Main7.java
import java.util.Scanner;

/**
 * Created 2017/05/25.
 */
public class Main7 {
  public static void main(String[] args){
    String word = "123 test";
    Scanner sc = new Scanner(word);
    int num     = sc.nextInt();
    String word2 = sc.next();

    //String line = sc.nextLine();
    //ちゃんとエラーが出る
    //Exception in thread "main" java.util.NoSuchElementException: No line found

    System.out.println(word);
    //実行結果:123test

    System.out.println(num);
    //実行結果:123

    System.out.println(word2);
    //実行結果:test

    //System.out.println(line);
    //実行結果: 上のエラーのせいでそもそも実行されない

  }
}
実行結果その1
123 test
123
test

コメントでも書いてありますが、.nextLine();のコメント外して実行するとエラーが出ます

実行結果その2(.nextLine();のコメント外して実行する)
Exception in thread "main" java.util.NoSuchElementException: No line found
    at java.util.Scanner.nextLine(Scanner.java:1540)
    at Java.Nexts.Main7.main(Main7.java:15)

これで真相判明・・・・・・とは行かず
ちゃんと「行がないよ!」っていう風にエラーを返してくれます
ところがちょっと追記するとエラーが出なくなります

Main8

Main8.java
import java.util.Scanner;

/**
 * Created 2017/05/25.
 */
public class Main8 {
  public static void main(String[] args){
    String word = "123 test\n";
    Scanner sc = new Scanner(word);
    int num     = sc.nextInt();

    String word2 = sc.next();

    String line = sc.nextLine();

    System.out.println(word);//
    //実行結果:123test
    //
    //上に空白(改行)が入る

    System.out.println(num);
    //実行結果:123

    System.out.println(word2);
    //実行結果:test

    System.out.println(line);
    //実行結果: (空白)
    //空白だけど、実際には空行を読み込んでいるので、Lineが存在していないわけじゃない→エラーを吐かないのだ!
    //ここのSystem.out.println(line);にデバッグをつけて実行すると非常に分かりやすいぞ!

  }
}
実行結果
123 test

123
test

はい、これが真相です
String wordの所で改行コードの「\n」を追記するのです
そうすると、word2までスキャンした後、Main7とは異なり、改行が残ることになるので
String line = sc.nextLine();が改行(空行)として読み込み
結果としてlineには空文字が入るのです

これはIDEのデバッグ機能を使うと視覚的に良く分かります
nextline.png
lineに""が入ってるのが分かります

つまり何が起きていたのか

これでやっとMain1のコードが変な挙動を示したのかがわかります
1:まず数値入力の時に2打った後にエンターを押しているので、scの中身に「2\n」が入力される
2:int num = sc.nextInt();にてスキャンした時に、num = 2が格納され、scの中身は「\n」だけになる
3:ループ開始後、String word = sc.nextLine();は現在残ってるscの中身「\n」を読み込み、
 空文字としてaryに格納する
4:scの中身が空になったので、ここで新しい入力の「a」を読み込んで格納する
5:この時点で2回処理を行ってるのでループが止まる
6:aryの中身が[,a]になる

結論的には
・Scannerは現在居る所(中身が残ってる部分)の値を値を返す
・.nextLine()は空行(空白)も読み込む
のあわせ技で起きていた不具合だったわけです

じゃあどうすれば理想的な動きになるのか

根本的な解決はscannerを二つ作ることです
そうすれば数値入力時の残骸を読み込む事がなくなるので

Main4.java
import java.util.ArrayList;
import java.util.Scanner;

/**
 * Created 2017/05/25.
 * やっと原因が分かった感
 */
public class Main4 {
  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    Scanner sc2 = new Scanner(System.in);
    ArrayList<String> ary = new ArrayList<String>();
    int num = sc.nextInt();
    for (int i = 0; i < num; i++) {
      String word = sc2.nextLine();
      ary.add(word);
    }
    System.out.println(ary);
  }
入力
2
a
b
実行結果
[a, b]

完璧ですね!

また、入力する文字列に空白区切りでないと分かっているならば
.next()にするだけでも同じ結果が得られます

Main2.java
import java.util.*;

/**
 * Created 2017/05/23.
 */
public class Main2 {
  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    ArrayList<String> ary = new ArrayList<String>();
    int num = sc.nextInt();
    for (int i = 0; i < num; i++) {
      String word = sc.next();
      ary.add(word);
    }
    System.out.println(ary);
  }
}
入力
2
a
b
実行結果
[a, b]

ただMain2のコードは空白区切りの文章を打った時に意図しない挙動になる可能性があるので注意しましょう

2
a b c
実行結果
[a, b]

2017/5/25 追記
コメントにてもっとスマートな解決作をいただきました

Main9.java
import java.util.ArrayList;
import java.util.Scanner;

/**
 * Created 2017/05/25.
 * もっとスマートなやり方をQiitaで教えてもらう
 */
public class Main9 {
  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    ArrayList<String> ary = new ArrayList<String>();
    int num = Integer.parseInt(sc.nextLine());    //Integer.parseIntを使って完全な数値に変換してしまえば、nextLineで読み込める!
    for (int i = 0; i < num; i++) {
      String word = sc.nextLine();
      ary.add(word);
    }
    System.out.println(ary);
  }
}
入力
2
a
b
実行結果
[a, b]

まず.nextLine()は読み込んだ結果をString型として扱います
そのため
int num = sc.nextLine();
とそのまま書くと
Error:(13, 26) java: 不適合な型: java.lang.Stringをintに変換できません:
と型の不整合がおき、int numに格納することが出来ません
しかし
int num = Integer.parseInt(sc.nextLine());
と記述することで、.nextLine()で読み込んだ値を数値に変換できるため、int numに格納することが可能になります!
こちらの書き方の方が、スキャナーを複数用意する必要がなく
さらに、入力された行毎に.nextLine()を呼び出しているという風になり、コードも非常に分かりやすくなります

感想

Java面倒くせえ
冗談はさておき
入力したい→ Scanner sc = new Scanner(System.in);
行単位で読み込みたい→.nextLine();
を使えばいいやっていう、コピペ的な考えがあったのは否定できません。
今回の事で、Scannerへの本質的な理解がかけていたというのが良く分かりました
まだまだJavaは分からない事だらけなので、今後もしっかり勉強したいと思います

2018/01/19
久々に読み直してたら流石に気になったので、ひとつ訂正
新しいスキャナーを作る必要は無く、単純に空白を一度スキャンさせれば問題無く動作します

ScanSample.java
import java.util.ArrayList;
import java.util.Scanner;

/**
 * Created 2018/01/19.
 * 訂正
 */
public class ScanSample {
  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    ArrayList<String> ary = new ArrayList<String>();
    int num = sc.nextInt();
    String blank = sc.nextLine();
    for (int i = 0; i < num; i++) {
      String word = sc.nextLine();
      ary.add(word);
    }
    System.out.println(ary);
  }
}
入力
2
a
b
実行結果
[a, b]

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
110