ミズノブログ

ミズノです。プログラミング・子育て・経年変化するもの など好きなことを雑多に書きます。

try-with-resourcesでリソース解放されないパターン

はじめに

Java7から"try-with-resources"構文が追加されました。 ファイルやDBアクセスしたあとのリソース解放を自動で行ってくれる大変便利な機能で、解放し忘れをなくし、コードをすっきりさせることができます。 ただし、書き方によってリソースが解放されないパターンがあったので紹介します。

具体的には以下のような場合です。 リソース解放の対象クラスをネストさせてインスタンス生成した場合、コンストラクタで例外が発生するとリソース解放されません。

File file = new File("out.txt");
// PrintWriterがインスタンス生成に失敗すると、BufferedWriter・FileWriterが解放されない
try(PrintWriter pw = 
        new PrintWriter(new BufferedWriter(new FileWriter(file)));) {
    // 処理
}
// ・・・

検証

各Writerクラスにログを仕込み、どのような動作をするか検証してみました。

public class Test {
    public static void main(String[] args) {

        File file = new File("out.txt");
        try (PrintWriter pw = 
                new PrintWriterWrapper(
                new BufferedWriterWrapper(
                new FileWriterWrapper(file)));) {
            System.out.println("func");
        } catch (Exception e) {
            System.out.println("catch:" + e);
        } finally {
            System.out.println("finally");
        }
    }

    // 以下、ログを追加したラッパークラス

    public static class PrintWriterWrapper extends PrintWriter {

        public PrintWriterWrapper(Writer out) {
            super(out);
            System.out.println("new PrintWriter");
        }

        @Override
        public void close() {
            System.out.println("close PrintWriter");
            super.close();
        }
    }

    public static class BufferedWriterWrapper extends BufferedWriter {

        public BufferedWriterWrapper(Writer out) {
            super(out);
            System.out.println("new BufferedWriter");
            throw new RuntimeException();
        }
        @Override
        public void close() throws IOException {
            System.out.println("close BufferedWriter");
            super.close();
        }
    }

    public static class FileWriterWrapper extends FileWriter {

        public FileWriterWrapper(File file) throws IOException {
            super(file);
            System.out.println("new FileWriter");
        }

        @Override
        public void close() throws IOException {
            System.out.println("close FileWriter");
            super.close();
        }
    }
}

処理が正常終了する場合、作成したインスタンスを逆順でリソース解放(closeメソッド実行)しています。

// ・・・
// ネストでインスタンス生成する
try (PrintWriter pw = 
        new PrintWriterWrapper(
        new BufferedWriterWrapper(
        new FileWriterWrapper(file)));) {
    System.out.println("func");
}
// ・・・

// 実行結果(close()が実行されている)
// new FileWriter
// new BufferedWriter
// new PrintWriter
// func
// close PrintWriter
// close BufferedWriter
// close FileWriter
// finally

ただしコンストラクタで例外が発生した場合、内包するインスタンスに対するリソース解放がされません。

// ・・・
// ネストでインスタンス生成する
try (PrintWriter pw = 
    new PrintWriterWrapper(
    new BufferedWriterWrapper(
    new FileWriterWrapper(file)));) {
  System.out.println("func");
}

// ・・・

// PrintWriterWrapperのコンストラクタで例外発生させる
public PrintWriterWrapper(Writer out) {
    super(out);
    System.out.println("ERROR!! new PrintWriter");
    thorow new RuntimeException();
} // ・・・

// 実行結果(close()が実行されない)
// new FileWriter
// new BufferedWriter
// ERROR!! new PrintWriter
// catch:java.lang.RuntimeException
// finally

ポイントは以下の2点です。

  • "try句で変数として宣言されたインスタンス"が自動リソース解放の対象となる
  • try句でインスタンス生成する際、コンストラクタで例外が発生した場合はcloseメソッドが実行されない

検証1では、変数pwのcloseメソッドが実行され、内包するBufferedWriter、FileWriterを連鎖的にcloseしています。 検証2では変数pwのcloseメソッドが実行されず、内包するインスタンスも自動リソース解放の対象となっていないためそのまま残ってしまいます。

解決法:ネストせず個別に変数定義する

結論として、コンストラクタで例外が発生しないことが明白である場合以外は個別に変数定義するのが良さそうです。 以下の例ではPrintWriterのインスタンス生成に失敗した場合もBufferedWriter、FileWriterのcloseメソッドが実行されています。 (FileWriterのcloseメソッドが2回実行されているのは、BufferedWriterのcloseメソッドから連鎖的に実行されたのと変数fwとして宣言したため自動リソース解放の対象となっているためです。)

// ・・・
// 個別にフィールドを宣言し、それぞれインスタンス生成する
try ( FileWriter fw = new FileWriterWrapper(file);
    BufferedWriter bw = new BufferedWriterWrapper(fw);
    PrintWriter pw = new PrintWriterWrapper(bw);) {
  System.out.println("func");
}

// ・・・

// PrintWriterWrapperのコンストラクタで例外発生させる
public PrintWriterWrapper(Writer out) {
    super(out);
    System.out.println("ERROR!! new PrintWriter");
    thorow new RuntimeException();
}
// ・・・

// 実行結果(close()が実行されている)
// new FileWriter
// new BufferedWriter
// ERROR!! new PrintWriter
// close BufferedWriter
// close FileWriter
// close FileWriter
// catch:java.lang.RuntimeException
// finally