安全なjava.io

よく見かけるパターン

public void foo() throws IOException {
    OutputStream out = new FileOutputStream("foo.txt");
    try {
        //いろいろ
    } finally {
        out.close();
    }
}

これは良くない。
closeで例外が起こると、tryブロック内でthrowされたものを上書きしてしまう。
Errorでさえも闇に葬ってしまうのでかなりよろしくない。
IO処理に限らず、Errorの事を忘れて例外の発生するコードをfinally内に書いてしまいがちなので気をつけたい。

public void foo() throws IOException {
    OutputStream out = new FileOutputStream("foo.txt");
    try {
        //いろいろ
    } finally {
        try {
            out.close();
        } catch(Exception e) {
            //ログに出す
        }
    }
}

これも良く見るパターン。
closeの例外はログに出して終わりと割り切ってしまえば、特に問題なく使える。
Errorが消えることも無い。
ログすらもいらんと、IOUtils#closeQuietlyを使うこともある。


手間が増えるので、これ以上の事をしているコードはあまり見ない。
面倒くさがらずこの条件で書いてみる。

  • Errorは即throw
  • tryブロック内で例外が出るとそれをthrow。closeも試みる。
  • closeだけで例外が発生した場合はそれをthrow
public void foo() throws IOException {
    OutputStream out = new FileOutputStream("foo.txt");
    try {
        // いろいろ
    } catch (IOException e) {
        try {
            out.close();
        } catch (Exception e2) {
        }
        throw e;
    } catch (RuntimeException e) {
        try {
            out.close();
        } catch (Exception e2) {
        }
        throw e;
    }
    out.close();
}

そう面倒でもなかった。
finallyを使うとフラグが必要になるので、使わないほうが楽。


毎回書くにはちょっと長いのでTemplateMethodにしてみる。

public abstract class Closing<T extends Closeable> {

    public Closing(T closeable) throws IOException {
        try {
            process(closeable);
        } catch (IOException e) {
            close(closeable, e);
        } catch (RuntimeException e) {
            close(closeable, e);
        }
        closeable.close();
    }

    private <E extends Exception> void close(Closeable closeable, E exception) throws E, IOException {
        try {
            closeable.close();
        } catch (IOException e) {
            throw exception;
        } catch (RuntimeException e) {
            throw exception;
        }
    }

    public abstract void process(T closeable) throws IOException;
}

こんな使うときはこんな感じ。

new Closing<FileOutputStream>(new FileOutputStream("foo.txt")) {
    public void process(FileOutputStream out) throws IOException {
        // いろいろ
    }
};


http://www.kawaz.jp/diary/?200502b&to=200502181#200502181
http://d.hatena.ne.jp/odz/20061016/1160974869
こちらで言及されている、コンストラクタで例外→close忘れもネストさせれば発生しない。

new Closing<FileOutputStream>(new FileOutputStream("foo.txt")) {
    public void process(FileOutputStream out) throws IOException {
        new Closing<Writer>(new OutputStreamWriter(out)) {
            public void process(Writer writer) throws IOException {
                // いろいろ
            }
        };
    }
};


テキストファイルコピー&文字数カウント&ダイジェスト取得メソッド

public static int foo(File src, final File dst, String encoding, final MessageDigest md) throws IOException {
    final int[] count = new int[1];
    new Closing<FileInputStream>(new FileInputStream(src)) {
        public void process(FileInputStream in) throws IOException {
            new Closing<Reader>(new InputStreamReader(new DigestInputStream(in, md))) {
                public void process(final Reader reader) throws IOException {
                    new Closing<FileOutputStream>(new FileOutputStream(dst)) {
                        public void process(FileOutputStream out) throws IOException {
                            new Closing<Writer>(new OutputStreamWriter(out)) {
                                public void process(Writer writer) throws IOException {
                                    char[] cbuf = new char[2048];
                                    int len = 0;
                                    while ((len = reader.read(cbuf)) != -1) {
                                        writer.write(cbuf, 0, len);
                                        count[0] += len;
                                    }
                                }
                            };
                        }
                    };
                }
            };
        }
    };
    return count[0];
}

読みにくい・・