ブロックする InputStream を BufferedInputStream でラップしてはいけない
以下のコードを実行すると:
import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; public class InputStreamTest { static class MyInputStream extends InputStream { int pos; byte[] data = "HelloWorld".getBytes(); @Override public int read() throws IOException { System.out.println("pos:" + pos + " data.length:" + data.length); if (pos < data.length) { return data[pos++]; } else { synchronized (data) { try { System.out.println("locking..."); data.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } return -1; } @Override public int available() { return data.length - pos; } } static void read(InputStream is) throws IOException { int available = is.available(); byte[] data = new byte[available]; is.read(data); System.out.println(new String(data)); is.close(); } public static void main(String[] args) throws IOException { read(new MyInputStream()); read(new BufferedInputStream(new MyInputStream())); } }
以下のような結果になります。
HelloWorld locking...
MyInputStream から直接読み込む場合、available() で返された 10 バイトがきっちりと読み出せ、BufferedInputStream でラップすると、11 バイト目を読み出しに行き、ブロックしてしまいます。
これはなぜか。
通常、InputStream#read(byte[]) は、渡されたバイト配列サイズだけバイトを読み込んだら、メソッド呼び出しから復帰します。BufferedInputStream は、効率のため 8192 バイトを読み出してバッファリングしておき、その中から 10 バイトを呼び出し元に返そうとします。しかしもし、コンストラクタに与えられた InputStream がストリームからの読み出しをブロックしてしまうなら、10 バイトデータが読み出せたにも関わらず、メソッド呼び出しがブロックしてしまうことになります。
BufferedInputStream は、例えばファイルからの読み込みなどに適しています。たとえファイルが 8192 バイト未満でも、ストリームの終了で -1 が返されれば、そこでバッファリングは終了します。逆に向かないのは、ソケットやシリアルです。
BufferedInputStream はコンストラクタにバッファサイズを渡せるので、このサイズを小さく調整することで(端的に言うと 1 にする)ソケットやシリアルでも使用することが可能になりますが、それだと BufferedInputStream を使用している意味がありません。
なお、Oracle VM に関して言えば、read() を以下のように変えることで BufferedInputStream でラップされても正常に動作するようになります。
@Override public int read() throws IOException { System.out.println("pos:" + pos + " data.length:" + data.length); if (pos < data.length) { return data[pos++]; } throw new IOException(); // return -1 でもよい }
これは、java.io.InputStream#read(byte[], int, int) が戻り値 -1 と IOException を無視する実装になっているためです。