Posted at

標準入力の罠とか

More than 3 years have passed since last update.


Questionとか

いきなりですがクイズです。

さて、以下のコードを実行した場合、どんなExceptionが起こるでしょうか。


Test.java

import java.util.Scanner;

public class Test {
public static void main(String[] args) {
System.out.println("終了は「end」を入力");
while(true){
try(Scanner scan=new Scanner(System.in)){
String line=scan.nextLine();
if("end".equals(line))break;
System.out.println(line);
}
}
}
}


Answerとか

パッと見、そもそもExceptionになりそうもないのですが、実は致命的なミスをしているため、何か入力するとscan.next()でExceptionが発生します。

そのExceptionはNoSuchElementExceptionです。

いみわかんなーい!

ごもっとも。

そもそもNoSuchElementException自体かなりのレアExceptionで、あんまり見たことないですし。

JavaDocを見ると、nextLine()のNoSuchElementExceptionに関しては以下のように書かれています。


NoSuchElementException - 行が見つからなかった場合


うわ、役に立たない説明!

この説明でわかったら逆に凄いですね!


説明とか

実際に何が起こったのかをコードを追って確認しましょう。

このプログラムはJava7で導入されたtry-with-resource構文が使用されています。

try-with-resource構文はtryブロック終了時(finally時)に暗黙的にリソースをcloseします。

ですから、このブロックをJava6風に書くと

        while(true){

Scanner scan=null;
try{
scan=new Scanner(System.in);
String line=scan.nextLine();
if("end".equals(line))break;
System.out.println(line);
}finally{
scan.close();
}
}

となります。

これでわかりましたね。

try-with-resouceによる暗黙のcloseが原因で、closeされたInputStream(=System.in)に対してnextLine()を発行したせいで、エラーになったのです。


修正とか

このコードを直すなら

        System.out.println("終了は「end」を入力");

try(Scanner scan=new Scanner(System.in)){
while(true){
String line=scan.nextLine();
if("end".equals(line))break;
System.out.println(line);
}
}

となりますが、(これがまた恐ろしいルールですが)closeさえしなければScannerは何度生成しても構わないので

        while(true){

Scanner scan=new Scanner(System.in);
String line=scan.nextLine();
if("end".equals(line))break;
System.out.println(line);
}

と書いても直ります。


結論とか

こんなミスは普通はしないのですが、初心者さんで「リソースは必ずcloseしなきゃあかんねん」みたいな原理原則をきっちり守るとかえってエラーになる上に、そのエラーの原因がさっぱりわからなくて途方に暮れること請け合いです。

おまけにしょっぱなに正しい値(「end」という文字列)を入れるとエラーにならないため、「正しい値が入力されるまでループ」なんて処理を書かせて、正常ケースしかチェックしてないと潜在バグに気づかないという恐ろしいことが起こり、対応する人もなんでエラーになってるんだこれ、と一緒に途方に暮れたりしますので、NoSuchElementExcepitonとかヘンテコなエラーが出たときは、この文を思い出すと良いことがあるかも知れません。