#概要
Javaで標準入力ストリームのScannerを1度close()すると、再度Scannerクラスのインスタンスを生成しても標準入力ができなくなります。
Scanner sc1 = new Scanner(System.in);
sc1.close();
Scanner sc2 = new Scanner(System.in);
String input = sc2.nextLine(); // NoSuchElementException
理由は、「標準入力ストリームはプログラム開始時に開き、sc生成時に開くわけではない。一方で、sc.close()すると標準入力ストリームが閉じてしまうため」です。
「当たり前じゃん」と思ったらそれでOK。
以下、もう少し詳しく説明します。
#私が誤解していた点
私は上述のエラーについて、「Scannerクラスのインスタンスを新しく生成しているんだから、読み込めてもいいのでは?」と思っていました。
これは、Scannerクラスとストリームの関係があいまいだったゆえの誤解です。
実際は new Scanner(System.in);
においてストリームはSystem.inです。
inはSystemクラスのstaticフィールドであり、InputStream型です。
/**
* The "standard" input stream. This stream is already
* open and ready to supply input data. Typically this stream
* corresponds to keyboard input or another input source specified by
* the host environment or user.
*/
public final static InputStream in = null;
#sc.close()すると何が起きる?
scというスキャナをクローズするわけですが、その際に可能ならストリームも閉じます。
つまり、以下のコードでは、標準入力ストリームSystem.inが閉じます。
Scanner sc = new Scanner(System.in);
sc.close();
Scanner.close()を読むと、確かにsource(ここではSystem.in)に対してsource.close()を試みています。
なお、sourceはScannerインスタンス生成時の引数です。また、InputStreamはCloseableです。
public void close() {
if (closed)
return;
if (source instanceof Closeable) {
try {
((Closeable)source).close();
} catch (IOException ioe) {
lastException = ioe;
}
}
sourceClosed = true;
source = null;
closed = true;
}
#サンプルコードを用いた解説
//プログラム開始
//既に標準入力ストリームは開いている
//スキャナを生成したが、ここで標準入力ストリームが開くわけではない
Scanner sc1 = new Scanner(System.in);
//スキャナを閉じると同時に、System.in(標準入力ストリーム)も閉じる
sc1.close();
//スキャナを生成したが、ここで標準入力ストリームが開くわけではない
Scanner sc2 = new Scanner(System.in);
//標準入力ストリームが開いていないのでエラー
String input = sc2.nextLine(); // NoSuchElementException
//プログラム終了時に標準入力ストリームが開いていた場合、自動で閉じる
//プログラム終了
System.inを引数にスキャナを生成しても標準入力ストリームは開きませんが、sc.close()するとSystem.inまでclose()するということです。
これが、標準入力のスキャナを1度closeしたら、(再度スキャナを生成しても)2度と標準入力できない理由です。
#eclipseが警告してくる理由(推測)
これ、実用上は「毎回Scannerクラスのインスタンスを生成する必要はない」「標準入力ストリームはプログラム終了時に自動的に閉じるから、明示的に閉じる必要はない」というだけの話です。
でも、eclipseで次のようなプログラムを書くと警告してくるんですよね。
public static void main(String args[]) {
inputName();
inputName();
}
static void inputName() {
Scanner sc = new Scanner(System.in);
//リソース·リーク:scが閉じられることはありません
String name = sc.nextLine();
System.out.println("名前:" + name);
}
「へぇ、じゃあ閉じます」と思ってinputNameの最後にsc.close()を追加すると、エラーが出るわけです。
なんなんだという感じですが、これはスキャナが標準入力ストリーム専用ではないことに起因すると推測しています。
つまり、scは明示的に閉じるべきストリームのスキャナかもしれないので、eclipseが保守的に警告を出してくれている、ということです。
標準入力ストリームの場合はこの警告は無視して構いません。
#未解決点
###①標準入力ストリームをclose()したあと、無理やり改めて開く方法は存在する?
そもそも「ストリームを閉じる」って具体的に何が起きている??
InputStream.close()のコードを呼んでも分からなかった(抽象クラスだし)
###②標準入力ストリームは具体的にはどう開いている?
「プログラム開始時に自動で開く」らしいけど、特別な実行が行われている?それとも通常のクラスと同様の処理がされている?
後者なら、System.inはstaticフィールドだから、Systemクラスが最初に使われる時にメモリにロードされるはず。でもSystemクラスはインスタンス生成不能だし、プログラム開始時にSystem.inを呼び出しているということ?
①と合わせて、ストリーム周りやJVMの本質的な理解が必要そう。