Java

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