イヤホンを新調したら雨夢楼と光のストライドがきれいに聴こえて嬉しいeller86です。洗濯機でイヤホンを洗濯してしまったときは絶望の淵に立たされていた気がしますが、あの絶望がこの喜びにつながるとは一体誰が想像したでしょうか。
さて前回のGuava紹介記事がそこそこ人気?だったようなので、I/Oにもう少し突っ込んだ内容も書いてみたいと思います。
イディオムを隠蔽化するFilesクラス
前回イディオムを排除できるクラスとしてCloseablesを紹介しましたが、Filesクラスもまた役立ちます。少なくとも以下のメソッドはおさえておくと役立つはずです。
- ファイルから読むためのBufferedReaderを作ってくれるnewReader(File, Charset)
- ファイルに書くためのBufferedWriterを作ってくれるnewWriter(File, Charset)
- Sun Java5以前のBufferedWriter#close()にはバグがあるので、Java5以前の環境で動作する可能性があるコードには使わないほうが無難です。
- 一時ディレクトリを作ってくれるcreateTempDir()
- 単体テストのお供に。
- InputStreamの内容をファイルに書いてくれるcopy(InputSupplier, File)
- Readerなどの内容をファイルに書いてくれるcopy(InputSupplier, File, Charset)
他にもFileからList
正規表現が使えるFilenameFilter
拡張子によるフィルタなどはとても普遍的な要求のはずですが、Javaでこれを実装するにはFilenameFilterの実装クラスをいちいち作らなければなりません。Guavaで提供されているPatternFilenameFilterを使えば、正規表現をコンストラクタに渡すだけで要求を満たすコードを書くことができます。ただまぁ劇的にラクというわけではないですね。
Writer#close()再考
突然ですが問題です。以下のコードの何が問題でしょうか?
Writer writer = createWriter(); try { writer.write( ... ); } finally { writer.close(); }
答えは例外のひき逃げ*1ですね。write()とclose()の両方が例外を投げた場合、write()が投げた例外が失われてしまいます。そしてwrite()が投げる例外のほうが先に生成されているので、より欲しい情報を持っている可能性が高いでしょう。
これを回避するためのコードは例えば以下のようになりますが、お世辞にも読みやすいとは言えません。Readerのようにclose()が投げる例外を無視できる*2なら良いのですが、Writerの場合はそうも行きません。
Writer writer = createWriter(); IOException thrown = null; try { writer.write( ... ); } catch (IOException e) { thrown = e; } finally { try { writer.close(); } catch (IOException e) { if (thrown == null) { thrown = e; } else { logger.warn("writer.close() throws IOException", e); } } } if (thrown != null) { throw thrown; }
そこでCloseables#close()を使うと、このように書けます。
Writer writer = createWriter(); boolean threw = true; try { writer.write( ... ); threw = false; } finally { // threwがfalseなら=write()が例外を投げていないなら、例外を投げる // threwがtrueなら=既に例外が投げられているなら、ログに書いて握りつぶす Closeables.close(writer, threw); }
完全にすっきりとは行きませんが、ぐっとマシにはなった気がします。
OutOfMemoryExceptionになりにくいByteArrayOutputStreamのようなもの
長い配列はOOMEの元凶になりかねないということがわかっていても、ByteArrayOutputStreamを使いたくなってしまうケースはあるものです。そこでFileBackedOutputStreamを使うと、巨大なデータを一時ファイルに退避してくれるようになります。
// 1024バイトを超えるデータはファイルに退避する FileBackedOutputStream oStream = new FileBackedOutputStream(1024); BufferedOutputStream buffer = new BufferedOutputStream(oStream); try { // どんなにたくさん書いてもOOMEが投げられない buffer.write( ... ); } finally { Closeables.closeQuietly(buffer); }
なお書き込んだデータはInputStreamとして簡単に取り出せます。
FileBackedOutputStream oStream = new FileBackedOutputStream(1024); BufferedOutputStream buffer = new BufferedOutputStream(oStream); // ... writing ... buffer.close(); InputStream iStream = oStream.getSupplier().getInput();
性能面では当然ByteArrayOutputStreamに劣る点、ファイルへの書き出しは特に暗号化されない点、ファイルへの書き出しはバッファリングされないのでBufferedOutputStreamでdecorateする必要がある点にだけ注意が必要です。