Java : try-with-resources文でリソースを自動的に解放

try-with-resources文は、リソースの解放漏れを防ぐ非常に強力な文法です。
本記事では、そんなtry-with-resources文を解説していきます。


コード例

さっそくコード例を見てみましょう。

例としてwriteToFileメソッドを用意しました。
指定したファイルに123という数値を書き込みます。

public void writeToFile(Path path) throws IOException {
    final var os = new FileOutputStream(path.toFile());
    os.write(123);
    os.close();
}

実はこのメソッドには問題があります。
わかりますでしょうか?

public void writeToFile(Path path) throws IOException {
    final var os = new FileOutputStream(path.toFile());
    os.write(123); // ここでIOExceptionが発生すると…
    os.close(); // ★closeが呼び出されません。
}

書き込み時にIOExceptionが発生するとFileOutputStreamのcloseが呼びされず、ファイルが開きっぱなしになってしまいます。
これをリソースリークといいます。

補足

  • 本来はFileOutputStreamを使うよりFiles.newOutputStreamを使うことをおすすめします。
    今回はリソースリークの問題が確認しやすいFileOutputStreamを使用しました。

try-with-resources文

14.20.3. try-with-resources
14.20.3.1. Basic try-with-resources
...
try (T #r = VariableAccess ...) {
   Block
}

公式のJava言語仕様ではChapter 14に説明があります。

それでは先ほどのコードを改善してみましょう。
リソース解放したい変数をtry(...)の部分で宣言するだけです。

public void writeToFile(Path path) throws IOException {
    try (final var os = new FileOutputStream(path.toFile())) {
        os.write(123); // ここでIOExceptionが発生しても…
    }
    // ★スコープを抜けたときに自動でcloseを呼び出してくれます。
}

os.closeをコード上で呼び出す必要はなくなります。
さらにtryのスコープを抜けたら自動でcloseしてくれるようになります。

同様のことをtry-with-resources文を使わないで実現するには、以下のようになります。

public void writeToFile(Path path) throws IOException {
    final var os = new FileOutputStream(path.toFile());
    try {
        os.write(123);
    } finally {
        os.close();
    }
}

AutoCloseableインタフェース

try-with-resources文で自動解放できるのは、AutoCloseableインタフェースを実装しているクラスだけです。

標準APIには、さまざまなクラスやインタフェースがAutoCloseableを実装しています。
それらのクラスは基本的にはcloseが必要です。
積極的にtry-with-resources文を使いリソースの解放漏れがないように注意しましょう。

クラス構成

リソースリークは難解なバグを発生させる

リソースリーク…実際にはどのような問題が起こるのでしょうか?
先ほどの問題のあるコード例に戻りましょう。

public void writeToFile(Path path) throws IOException {
    final var os = new FileOutputStream(path.toFile());
    os.write(123); // ここでIOExceptionが発生すると…
    os.close(); // ★closeが呼び出されません。
}

書き込み時にIOExceptionが発生することは十分ありえます。
例えば書き込み先のストレージ容量が足りなくなった場合です。

その後、そのファイルを操作しないのであれば、プログラムは問題なく動いているように見えるかもしれません。
しかしそれは間違いです。

そのファイルを削除や移動しようとすると例外が発生します。

try {
    // writeでIOExceptionが発生したと仮定します。
    writeToFile_ng(path);
} catch (IOException e) {
    // 書き込みに失敗したのでファイルを削除したい、とします。

    // "FileSystemException: R:\java-work\test.data: プロセスはファイルにアクセスできません。別のプロセスが使用中です。"
    // が発生します。
    Files.delete(path);
}

リソースリークでやっかいなのは、リークした瞬間には問題が発生せず、後になってから問題が発生することです。

上記の例では、リークが発生した場所と、問題(FileSystemException)が発生した場所が比較的近いので、問題解析は楽かもしれません。

ただ、問題の発生場所が離れていることもよくあります。
さらに、正常系では問題は発生せず、IOExceptionが発生するなど特定の条件でしか発生しないことも多いです。(再現性が低い)

これらの要素が重なり…問題解析は難解になることが多いです。

そのためAutoCloseableを実装しているクラスの扱いには常に注意し、try-with-resources文で解放漏れを予防するのが大事になります。

閑話

  • リソースリークで有名なのはメモリリークです。
    少し古めのCやC++では苦しめられた方もいらっしゃるのではないでしょうか。
    (最近はスマートポインタが導入されて、そのあたりはだいぶ改善したようです)

  • Javaではガベージコレクションという機能によりメモリリソースは(基本的に)自動で解放してくれます。
    そのためメモリリークで苦しむことはほぼないです。Javaの良いところですね。
    (循環参照など、場合によってはJavaでもメモリリークすることはあります)

独自に定義したクラスをtry-with-resources文で使う

とはいえ、いつもいつもメソッド内の閉じたスコープ内でリソースを使うとは限りません。
try-with-resources文が使えない場面もあるでしょう。

// メソッド内の閉じたスコープでtry-with-resources文を使うのが理想です。
public void writeToFile(Path path) throws IOException {
    try (final var os = new FileOutputStream(path.toFile())) {
        os.write(123);
    }
}

例えば、頻繁にファイルアクセスがあり、毎回ファイルのオープン・クローズを繰り返すとパフォーマンスに影響するとき、などでしょうか。

ちょっと実用的なクラスではないですが、以下のようなCountWriterクラスがあるとします。

// writeCountを呼び出すたびに、1, 2, 3, 4...とファイルに書き込むクラス。
// 最後にcloseでリソースを解放します。
public class CountWriter {

    private final OutputStream os;
    private byte count;

    public CountWriter(Path path) throws IOException {
        this.os = Files.newOutputStream(path);
    }

    public void writeCount() throws IOException {
        count++;
        os.write(count);
    }

    public void close() throws IOException {
        os.close();
    }
}

CountWriterを使う例です。

final var countWriter = new CountWriter(path);

countWriter.writeCount();
countWriter.writeCount();
countWriter.writeCount();

System.out.println(Arrays.toString(Files.readAllBytes(path))); // [1, 2, 3]

countWriter.close();

これでは冒頭のコード例と同じで、リソースリークの可能性がありますね。
writeCountで例外が発生したときです。

このようなケースでは、リソースを管理しているクラスにAutoCloseableを実装することを検討してみましょう。
今回の例ではCountWriterにAutoCloseableを実装します。

public class CountWriter implements AutoCloseable {

    private final OutputStream os;
    private byte count;

    public CountWriter(Path path) throws IOException {
        this.os = Files.newOutputStream(path);
    }

    public void writeCount() throws IOException {
        count++;
        os.write(count);
    }

    @Override
    public void close() throws IOException {
        os.close();
    }
}
try (final var countWriter = new CountWriter(path)) {
    countWriter.writeCount();
    countWriter.writeCount();
    countWriter.writeCount();

    System.out.println(Arrays.toString(Files.readAllBytes(path))); // [1, 2, 3]
}

これで、CountWriterクラスもtry-with-resources文で使えるようになりました。
このように、なるべくtry-with-resources文で使えるように改善できないか検討してみることも大事です。

まとめ

  • 初めて使うクラスやインタフェースは、API仕様でAutoCloseableを実装しているかどうか確認しましょう。
  • 実装している場合は、使い終わったら必ずcloseを呼び出すようにしましょう。
  • できる限りtry-with-resources文を使いましょう。

関連記事

ページの先頭へ