Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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

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

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

検証

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

検証プログラム.java
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メソッド実行)しています。

検証1_ネスト_正常パターン.java
// ・・・
// ネストでインスタンス生成する
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

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

検証2_ネスト_コンストラクタで例外.java
// ・・・
// ネストでインスタンス生成する
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として宣言したため自動リソース解放の対象となっているためです。)

個別に変数定義.java
// ・・・
// 個別にフィールドを宣言し、それぞれインスタンス生成する
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

nesheep5
サーバサイドエンジニアをやっています。 SIer→DeNA→freee Go/Ruby/Java
https://blog.shogo-mizuno.me
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした