ブロックする 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 を無視する実装になっているためです。