🤖

今更ながらJavaのI/Oストリームを整理する

2020/10/03に公開

JavaのI/Oストリーム周りのクラスを今まで雰囲気でなんとなく使っていて何がどういう役割を果たすのか混乱してきたので整理する記事です。

前提

今回記載するJava APIはJava 11を元にしています。

今回整理したいストリーム周りのクラス

今回整理するのは以下のクラスを対象とします。

  • InputStream
  • OutputStream
  • FileInputStream
  • FileOutputStream
  • BufferedInputStream
  • BufferedOutputStream
  • Reader
  • Writer
  • InputStreamReader
  • OutputStreamWriter
  • FileReader
  • FileWriter
  • BufferedReader
  • BufferedWriter

そもそもストリームとは

ストリーム(英: stream)とは、連続したデータを「流れるもの」として捉え、そのデータの入出力あるいは送受信を扱うことであり、またその操作のための抽象データ型を指す[1]。出力ストリーム (output stream) を利用してデータの書き込みを行ない、また入力ストリーム (input stream) を利用してデータの読み出しを行なう。ファイルの入出力を扱うもの、メモリバッファの入出力を扱うもの、ネットワーク通信を扱うものなどさまざまなものがある。

ストリーム (プログラミング) - Wikipedia

ストリームとはファイルやネットワークやメモリに対するデータの読み出し・書き出し、またはそれを操作できるインターフェースのことを言っているという理解。

まずInputStream/OutputStreamとReader/Writerで分ける

上記クラスのうち、InputStream, OutputStream, Reader, Writerは抽象クラスです。そのため、全てのストリーム系のクラスは基本この4つのどれかを継承しています。

InputStream
 |-- FileInputStream
 |-- ByteArrayInputStream
 |-- FilterInputStream
      |-- BufferedInputStream

OutputStream
 |-- FileOutputStream
 |-- ByteArrayOutputStream
 |-- FilterOutputStream
      |-- BufferedOutputStream

Reader
 |-- InputStreamReader
      |-- FileReader
 |-- BufferedReader
 |-- StringReader

Writer
 |-- OutputStreamWriter
      |-- FileWriter
 |-- BufferedWriter
 |-- StringWriter
 |-- PrintWriter

InputStream/OutputStreamはバイト単位でデータを扱います。InputStreamは読み出しでOutputStreamは書き込みです。
Reader/Writerは文字単位でデータを扱います。Readerは読み出しでWriterは書き込みです。

以降それぞれを詳しく見ていきます。

InputStream/OutputStream

データをバイト単位で扱うストリームです。バイトストリームとも言います。
InputStream/OutputStreamはデータをバイト単位で扱うため、バイナリデータの読み書きで使われることが多いと思います。
テキストデータの読み書きにも使えますが、後述のReader/Writerを使ったほうがメリットがあるので文字データをReader/Writerを使いましょう。

InputStream/OutputStreamには以下のような実装クラスが存在します。

  • FileInputStream/FileOutputStream
  • ByteArrayInputStream/ByteArrayOutputStream
  • ZipInputStream/ZipOutputStream
  • BufferedInputStream/BufferedOutputStream

参考

Reader/Writer

データを文字単位で扱うストリームです。文字ストリームとも言います。
InputStream/OutputStreamはデータをバイト単位で扱うので例えば日本語のようなマルチバイトな文字を読み出す場合、指定バイト数によっては文字の途中までしか読みだせずに表示したときに文字化けを起こすことがあります。
日本語のようなマルチバイト文字を読み書きする場合はReader/Writerを使ったほうが良いです。というかシングルバイト文字でもわざわざInputStream/OutputStreamを選んで使うメリットはあまりないように思うので、基本的に文字を扱う場合はReader/Writerで良いと思います。

Reader/Writerには以下のような実装クラスが存在します。

  • InputStreamReader/OutputStreamWriter
  • FileReader/FileWriter
  • BufferedReader/BufferedWriter
  • StringReader/StringWriter
  • PrintWriter

参考

ここまででInputStream/OutputStream, Reader/Writerは整理できたので以降はそれぞれの実装クラスについていくつかピックアップしていきます。

InputStream/OutputStreamの実装クラス

FileInputStream/FileOutputStream

ファイルからデータを読み込む、またはファイルへデータを書き込むバイトストリーム。
ファイルに対してバイナリデータを読み書きする場合はこれを利用します。

実装例

byte[] readData = new byte[1024];
try (
        FileInputStream fis = new FileInputStream("./src/read.png");
        FileOutputStream fos = new FileOutputStream("./out/written.png")) {
    int readBytes;
    // readData配列の長さ(バイト数)分読み込んで書き込む
    // これをファイルの最後を読み込むまで繰り返す
    while ((readBytes = fis.read(readData)) != -1) {
        fos.write(readData, 0, readBytes);
    }
} catch (IOException e) {
    e.printStackTrace();
}

BufferedInputStream/BufferedOutputStream

データをバッファリングしながら読み書きするバイトストリーム。
上記のFileInputStream/FileOutputStreamはread/writeが行われた回数だけファイルへのI/Oが発生するため、効率が悪いです。
BufferedInputStream/BufferedOutputStreamはデータを一定量バッファに溜め込んで(バッファリングして)から処理するため、I/Oの回数を減らし効率的に読み書きを行うことができます。

使用する場合はInputStream/OutputStreamをそれぞれのコンストラクタに指定してストリームを生成します。

実装例

byte[] readData = new byte[1024];
try (
        // コンストラクタ引数にFileInputStreamを指定
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("./src/read.png"));
	// コンストラクタ引数にFileOutputStreamを指定
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("./out/written.png"))) {
    int readBytes;
    while ((readBytes = bis.read(readData)) != -1) {
        bos.write(readData, 0, readBytes);
    }
} catch (IOException e) {
    e.printStackTrace();
}

Reader/Writerの実装クラス

InputStreamReader/OutputStreamWriter

バイトストリームと文字ストリームの変換を行うストリーム。
言い換えるとInputStream, OutputStreamをそれぞれReader, Writerでラップしてあげることで利用者はバイトストリームではなく文字ストリームを扱えるようになります。

InputStreamReaderはバイトストリームから文字ストリームへの変換を行います。
ストリームからバイトを読み込むと指定されたcharsetで文字にデコードします。そのため利用者は文字ストリームとして読み込みを行うことができます。

OutputStreamWriterは文字ストリームからバイトストリームへの変換を行います。
ストリームへ文字を書き込むと指定されたcharsetでバイトにエンコードします。そのため利用者は文字ストリームとして書き込みを行うことができます。

使用する場合はInputStream/OutputStreamをそれぞれのコンストラクタに指定してストリームを生成します。

実装例(InputStreamReader)

char[] readData = new char[32];
try (
    // コンストラクタ引数にFileInputStreamを指定
    InputStreamReader isr = new InputStreamReader(new FileInputStream("./src/japanese.txt"))) { // 「私は日本人です」と書かれたファイルを読み込む
    int readChars = isr.read(readData, 0, 5); // 0文字目から5文字分読み込む
    System.out.println(readChars); // 5
    System.out.println(new String(readData)); // 私は日本人
} catch (IOException e) {
    e.printStackTrace();
}

FileReader/FileWriter

ファイルへの読み書きを行う文字ストリーム。
内部の実装的にはそれぞれnew InputStreamReader(new FileInputStream("...")), new OutputStreamWriter(new FileOutputStream("..."))をラップしているにすぎません。
これを少し簡略的に書けるReader/Writerになります。

実装例(FileReader)
やっていることは上記の実装例と同じになります。

char[] readData = new char[32];
try (FileReader reader = new FileReader("./src/japanese.txt")) { // 「私は日本人です」と書かれたファイルを読み込む
    int readChars = reader.read(readData, 0, 5); // オフセット0文字目から5文字分読み込む
    System.out.println(readChars); // 5
    System.out.println(new String(readData)); // 私は日本人
} catch (IOException e) {
    e.printStackTrace();
}

BufferedReader/BufferedWriter

データをバッファリングしながら読み書きする文字ストリーム。
上記のInputStreamReader/OutputStreamWriterやFileReader/FileWriterはread/writeが行われた回数だけファイルへのI/Oが発生するため、効率が悪いです。
BufferedReader/BufferedWriterはデータを一定量バッファに溜め込んで(バッファリングして)から処理するため、I/Oの回数を減らし効率的に読み書きを行うことができます。
仕組みはBufferedInputStream, BufferedOutpuStreamと同じです。

使用する場合はReader/Writerをそれぞれのコンストラクタに指定してストリームを生成します。

実装例(BufferedReader)

char[] readData = new char[32];
try (BufferedReader br = new BufferedReader(new FileReader("./src/japanese.txt"))) { // 「私は日本人です」と書かれたファイルを読み込む
    // まずバッファに、ある十分な量(デフォルトは8192文字分)読み込まれる
    // その後バッファから指定文字数(今回は5文字分)読み込む
    int readChars = br.read(readData, 0, 5); // オフセット0文字目から5文字分読み込む
    System.out.println(readChars); // 5
    System.out.println(new String(readData)); // 私は日本人
    
    // 以降はバッファから読み込む
    // バッファに格納されたデータより先のデータを読み込むときに再度十分な量をバッファに読み込んでからバッファから読み込む
    readChars = br.read(readData, readChars, 2);
    System.out.println(readChars); // 2
    System.out.println(new String(readData)); // です
} catch (IOException e) {
    e.printStackTrace();
}

余談1:BufferedReaderはどのタイミングでバッファリングを行っているか

OpenJDK 11.0.7でコードを追ってみると、readしたタイミングで行っていました。
以下のfillメソッドがバッファリング処理に該当しますが、バッファに格納されたデータより先のデータを読み込む場合に呼ばれていることがわかります。

private int read1(char[] cbuf, int off, int len) throws IOException {
    if (nextChar >= nChars) {
    /* If the requested length is at least as large as the buffer, and
       if there is no mark/reset activity, and if line feeds are not
       being skipped, do not bother to copy the characters into the
       local buffer.  In this way buffered streams will cascade
       harmlessly. */
        if (len >= cb.length && markedChar <= UNMARKED && !skipLF) {
            return in.read(cbuf, off, len);
        }
        fill();
    }
    if (nextChar >= nChars) return -1;
    if (skipLF) {
        skipLF = false;
        if (cb[nextChar] == '\n') {
            nextChar++;
            if (nextChar >= nChars)
                fill();
            if (nextChar >= nChars)
                return -1;
        }
    }
    int n = Math.min(len, nChars - nextChar);
    System.arraycopy(cb, nextChar, cbuf, off, n);
    nextChar += n;
    return n;
}

https://github.com/openjdk/jdk11u/blob/jdk-11.0.7-ga/src/java.base/share/classes/java/io/BufferedReader.java#L202-L229

バッファリング処理(fillメソッド)
https://github.com/openjdk/jdk11u/blob/jdk-11.0.7-ga/src/java.base/share/classes/java/io/BufferedReader.java#L128-L167

逆にバッファにデータがあればバッファから読み込むだけになっているのでこれによってI/Oの回数を減らすことができています。

余談2:BufferedReaderはどの程度の量一度にバッファリングするか

OpenJDK 11.0.7で確認したところ、デフォルト値は8192文字数分となっていました。
https://github.com/openjdk/jdk11u/blob/jdk-11.0.7-ga/src/java.base/share/classes/java/io/BufferedReader.java#L88

ただ、この値はBufferedReaderのコンストラクタの第二引数で変更することが可能です。そのため一度に8192文字以上読み込みたい場合はこの値を変えると良いかもしれません。
https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/io/BufferedReader.html#<init>(java.io.Reader,int)

まとめ

InputStream/OutputStream Reader/Writer
役割 バイトストリームを扱う 文字ストリームを扱う
実装クラス FileInputStream/FileOutputStream BufferedInputStream/BufferedOutputStream InputStreamReader/OutputStreamWriter FileReader/FileWriter BufferedReader/BufferedWriter

Discussion