Java: closeQuietly considered harmful

Apache Commons IO は、入出力に関するユーティリティを集めたライブラリで、かつてに比べれば存在感が薄れているとはいえ、現在でも広く利用されている。
しかし、そんなライブラリの中にも、使用すべきでないものがある。closeQuietly という名前でオーバーロードされている、IOUtils クラスの一連の static メソッドがそれだ。


API リファレンスに載っているコードサンプルは、次のようなものだ。このサンプル通りであれば問題なさそうではある (Exception で catch しているところがちょっとモヤモヤするが、そこは本題と関係ない)。

char[] data = new char[1024];
Reader in = null;
try {
    in = new FileReader("foo.txt");
    in.read(data);
    in.close(); //close errors are handled
} catch (Exception e) {
    // error handling
} finally {
    IOUtils.closeQuietly(in);
}

しかし僕の予測では、次のように使われている場合が少なくないはずだ。

char[] data = new char[1024];
Reader in = null;
try {
    in = new FileReader("foo.txt");
    in.read(data);
} catch (Exception e) {
    // error handling
} finally {
    IOUtils.closeQuietly(in);
}

違いは、コードサンプルの中で "close errors are handled" とコメントが書いてあった行を削ってしまったこと。close で例外が起きることなんてそうそうないから、最初から closeQuietly で閉じてもいいじゃん、と考えたわけだ。あるいは、そもそも API リファレンスを呼んでいなくて、「closeQuietly って、名前から察するに、close の例外投げないバージョンみたいなのでしょ? close の代わりに closeQuietly を使えば、finally の中でさらに try/catch を書かなくてよくなるんでしょ?」と考えたのかもしれない。
そういった判断は実際には誤りで、close で例外が起きることもそれなりにある。closeQuietly で無条件に例外を握りつぶしてしまうと、「ファイルへの書き込みがうまく行っていないが、ログを見ても何も出ていない」という嫌な事態に陥ってしまう。

  • 典型的な例として、BufferedOutputStream や BufferedWriter の close を呼ぶと、ヒープ上のバッファに残っていたデータが書き出される (フラッシュされる) が、例えばこの時点でディスクの空き容量がなかった場合には close から IOException が投げられる。
  • また、似ているけれどもう少し特殊な例として、NFS 経由での書き込みがある。NFSv4 ではアプリケーションから書き込まれたデータをできるだけクライアント側でバッファリングするようになっていて、バッファが足りなくなった時点で初めてサーバへのデータ送信が行われるが、もうひとつ、ファイルがクローズされた時にも未送信分がサーバに送られる。バッファリングされていたデータの量が多いと、ここで数秒から数十秒かかることがあり、途中で失敗した場合は Java アプリケーションでは close が IOException を投げるという結果になる。加えて、失敗はしなくても、データ送信中にシグナルによる割り込みが行われた場合も、IOException が投げられることがある。
    • 割り込みが原因の場合は、例外メッセージが "Interrupted System Call" になる。InterruptedIOException ではなく IOException が投げられるのは、JDK/JRE の実装がそうなっているからだが、理由は close(2) の特殊性にあるかもしれない。すなわち、Linux の一般的な I/O 系システムコールは、失敗した場合のエラーコードが割り込みを示す EINTR であった場合はアプリケーション側から同じ引数でもう一度呼ぶべしということになっているが、close(2) の場合は EINTR であっても再度呼び出してはいけない。

API リファレンスを読んでその通りに書いたつもりであっても、対象が増えたときに次のように書いてしまう人もいそうだ。

char[] data = new char[1024];
Reader in = null;
Writer out = null;
try {
    in = new FileReader("foo.txt");
    out = new FileWriter("bar.txt");
    int n = in.read(data);
    out.write(data, 0, n);
    in.close();
    out.close();
} catch (Exception e) {
    // error handling
} finally {
    IOUtils.closeQuietly(in);
    IOUtils.closeQuietly(out);
}

まとめると、

  • closeQuietly は間違った使い方を誘発しがち
  • しかも、いざ問題が発生しても何の痕跡も残らないので危険

ということが言えると僕は思う。
Java SE 7 以降の場合は、try-with-resources を使うのがよい。記述が楽になる上に例外処理も正しく行われるといういいことずくめだからだ。
Java SE 6 の場合には、いくつか選択肢があるように思うが、誰もが納得するような書き方を今のところ僕は知らない。この記事ではタイトルからして closeQuietly に喧嘩を売っているので代案を示すのが筋だとは思うが、そういうわけでここでは深入りしないことにする。



[2012-11-28 00:43 追記] API リファレンスには問題のないサンプルコードが載っていると書いたけど、「でもこんなの前からあったっけ?」と思って確認してみたところ、2010 年に追加されたものだった。よく見ると Apache Commons IO のサイトにある "Best Practices" というページにも間違った例が載っている……。