Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

Guavaをどっぷり紹介(I/O編)

イヤホンを新調したら雨夢楼光のストライドがきれいに聴こえて嬉しいeller86です。洗濯機でイヤホンを洗濯してしまったときは絶望の淵に立たされていた気がしますが、あの絶望がこの喜びにつながるとは一体誰が想像したでしょうか。
さて前回のGuava紹介記事がそこそこ人気?だったようなので、I/Oにもう少し突っ込んだ内容も書いてみたいと思います。

イディオムを隠蔽化するFilesクラス

前回イディオムを排除できるクラスとしてCloseablesを紹介しましたが、Filesクラスもまた役立ちます。少なくとも以下のメソッドはおさえておくと役立つはずです。

他にも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する必要がある点にだけ注意が必要です。

*1:「[asin:4798118125:title=コーディングの掟]」より。例外処理周りで特に勉強になるので、Javaユーザーにはオススメの1冊です

*2:たぶんこの辺は諸説あって、Readerでも無視すべきでないとする方もいらっしゃるのでは