今更ながらJavaのI/Oストリームを整理する
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) を利用してデータの読み出しを行なう。ファイルの入出力を扱うもの、メモリバッファの入出力を扱うもの、ネットワーク通信を扱うものなどさまざまなものがある。
ストリームとはファイルやネットワークやメモリに対するデータの読み出し・書き出し、またはそれを操作できるインターフェースのことを言っているという理解。
まず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;
}
バッファリング処理(fillメソッド)
逆にバッファにデータがあればバッファから読み込むだけになっているのでこれによってI/Oの回数を減らすことができています。
余談2:BufferedReaderはどの程度の量一度にバッファリングするか
OpenJDK 11.0.7で確認したところ、デフォルト値は8192文字数分となっていました。
ただ、この値はBufferedReaderのコンストラクタの第二引数で変更することが可能です。そのため一度に8192文字以上読み込みたい場合はこの値を変えると良いかもしれません。
まとめ
InputStream/OutputStream | Reader/Writer | |
---|---|---|
役割 | バイトストリームを扱う | 文字ストリームを扱う |
実装クラス | FileInputStream/FileOutputStream BufferedInputStream/BufferedOutputStream | InputStreamReader/OutputStreamWriter FileReader/FileWriter BufferedReader/BufferedWriter |
Discussion