Javaのtry-with-resourcesでclose漏れを防ぐ方法を解説する記事のアイキャッチ。

Java

Javaのtry-with-resourcesとは|close漏れを防ぐ書き方

AIによる要約

try-with-resourcesは、tryの丸括弧内で取得したAutoCloseableを、正常終了でも例外でも自動的にcloseする構文です。finallyの手動closeより閉じ忘れや例外の上書きを防ぎやすくなります。複数リソースは宣言と逆順で閉じられ、本文とcloseの両方で例外が起きた場合、close側はsuppressed exceptionとして保持されます。ただし、フレームワークから借りたリソースを自分で閉じるかは所有者の契約を確認します。

新人SE
新人SE
finallyでcloseを書けば十分なのに、try-with-resourcesを使う理由は何ですか?
ポンコツSE
ポンコツSE
例外時にも確実に閉じ、close側の例外で元の原因を失いにくくするためです。コードも短くなります。

ファイル、ストリーム、JDBC接続などは、使い終わったらcloseする必要があります。閉じ忘れるとファイルを開けない、DB接続プールを使い切る、メモリやOS資源を消費するといった障害につながります。

try-with-resourcesを使えば多くの後始末をJavaへ任せられます。ただし、何でもcloseすればよいわけではありません。自分が生成して所有するリソースか、フレームワークから管理を委ねられたものかを確認します。

この記事のポイント

  • AutoCloseableを自動でcloseする構文
  • 正常時と例外時の両方でcloseされる
  • 複数リソースは宣言と逆順で閉じる
  • close例外はsuppressed exceptionとして残る
  • リソースの所有者を確認してから閉じる

基本の書き方

tryの丸括弧内でリソースを宣言し、そのリソースをtryブロック内で使います。ブロックを抜けると自動的にcloseされます。

try (BufferedReader reader = Files.newBufferedReader(path)) {
    return reader.lines().toList();
}

正常returnでも、read中のIOExceptionでもcloseされます。readerをfinallyで参照するため外側へ宣言する必要もありません。

AutoCloseableが対象

try-with-resourcesへ置けるのはAutoCloseableを実装した型です。InputStream、Reader、Writer、JDBCのConnection・Statement・ResultSetなどが該当します。

final class TemporaryDirectory implements AutoCloseable {
    private final Path path;

    @Override
    public void close() throws IOException {
        deleteRecursively(path);
    }
}

独自クラスへAutoCloseableを実装する場合、複数回closeしても安全か、close例外をどう扱うか、ライフサイクルが本当にブロック単位かを設計します。単なる後処理メソッドへ無条件に付けません。

finallyで手動closeする問題

古い書き方ではリソースを外側へ宣言し、finallyでnullチェックしてcloseします。コードが長く、close自体の例外が本文の例外を隠す可能性があります。

BufferedReader reader = null;
try {
    reader = Files.newBufferedReader(path);
    return reader.readLine();
} finally {
    if (reader != null) {
        reader.close();
    }
}

Java 7以降でAutoCloseableなら、原則としてtry-with-resourcesへ置き換えます。既存案件のJavaバージョンとコーディング規約は確認します。

複数リソースは逆順で閉じる

セミコロンで複数リソースを宣言できます。closeは最後に宣言したものから逆順です。外側のWriterが内側のOutputStreamへバッファを流すような構造で重要です。

try (OutputStream output = Files.newOutputStream(path);
     BufferedWriter writer = new BufferedWriter(
             new OutputStreamWriter(output, StandardCharsets.UTF_8))) {
    writer.write("hello");
}

ラッパー側のcloseが内部リソースも閉じる場合、両方を宣言する必要がないこともあります。API契約を確認し、所有関係を分かりやすくします。

suppressed exceptionを確認する

try本文とcloseの両方で例外が起きた場合、本文の例外が主例外として投げられ、close側はsuppressed exceptionに格納されます。元の障害原因を維持しやすい仕組みです。

catch (IOException e) {
    log.error("ファイル処理に失敗", e);
    for (Throwable suppressed : e.getSuppressed()) {
        log.warn("close時にも例外", suppressed);
    }
}

通常はロガーがsuppressedもスタックトレースへ出します。独自ログ整形で省略していないか、障害時の実ログを確認します。

既存変数も利用できる

Java 9以降では、実質finalな既存変数をtryの丸括弧内へ書けます。ただし、どこで生成され誰が閉じるかが分散すると所有者が分かりにくくなります。

BufferedReader reader = Files.newBufferedReader(path);
try (reader) {
    return reader.readLine();
}

可能なら取得とclose範囲を近づけます。呼び出し元から渡されたリソースをメソッド内で閉じるかはAPI契約に明記します。

JDBCでの利用例

JDBCを直接使う場合、Connection、PreparedStatement、ResultSetのcloseが必要です。DataSourceから取得したConnectionはcloseによってプールへ返却されることが多いです。

try (Connection connection = dataSource.getConnection();
     PreparedStatement statement = connection.prepareStatement(SQL)) {
    statement.setString(1, userId);
    try (ResultSet result = statement.executeQuery()) {
        return result.next() ? map(result) : Optional.empty();
    }
}

一方、SpringのJdbcTemplateや@Transactionalが管理する接続を低い層で勝手にcloseしないでください。フレームワークAPIを使う場合は、その管理契約に従います。

Streamを返すメソッドの注意

Files.linesなど、閉じる必要があるStreamがあります。try内で終端操作まで行わずStreamを外へ返すと、いつcloseするか呼び出し側へ移ります。

List<String> readAll(Path path) throws IOException {
    try (Stream<String> lines = Files.lines(path)) {
        return lines.toList();
    }
}

遅延評価のStreamを返す必要があるなら、呼び出し側がclose責務を理解できるAPIにします。単純な業務処理では、ブロック内でListへ確定させる方が安全です。

テストでcloseを確認する

独自AutoCloseableや重要な共通部品では、正常時と例外時の両方でcloseされたことを確認します。標準クラス自体ではなく、自作のライフサイクル契約をテストします。

@Test
void 処理中に例外でもcloseされる() {
    RecordingResource resource = new RecordingResource();

    assertThrows(RuntimeException.class, () -> {
        try (resource) {
            throw new RuntimeException("failed");
        }
    });

    assertTrue(resource.isClosed());
}

現場レビューでよくある指摘

リソース処理のレビューではtry-with-resourcesの構文だけでなく、誰が取得し、誰が閉じ、いつまで有効かという所有権を確認します。

// レビューコメント例
BufferedReaderはAutoCloseableです。手動finallyではなく
try-with-resourcesで取得とclose範囲をまとめてください。

// レビューコメント例
Files.linesのStreamを返しており、呼び出し側でcloseされません。
このメソッド内で終端操作まで行うか、close責務をAPIへ明記してください。

// レビューコメント例
このConnectionはSpringのトランザクション管理対象です。
Repository内で独自にcloseすると処理全体へ影響するため、管理契約を確認してください。

close漏れの修正では、正常終了だけでなく例外経路と繰り返し実行を確認します。接続プールやファイルハンドルの監視値が戻ることも結合環境で確認します。

提出前のセルフチェック

レビュー前に確認すること

  • 対象がAutoCloseableか
  • try-with-resourcesを使えるか
  • 正常時と例外時にcloseされるか
  • 複数リソースの所有関係とclose順を確認したか
  • suppressed exceptionがログへ残るか
  • 外から渡されたリソースを勝手に閉じていないか
  • フレームワーク管理の接続ではないか
  • Streamをclose前に外へ返していないか

Javaの例外とリソース管理を学ぶ参考書

try-with-resourcesは例外処理、ファイルI/O、JDBCと関係します。実践編でまとまって確認すると、構文だけでなく所有者と例外経路を判断しやすくなります。

書籍「スッキリわかるJava入門 実践編 第5版」の表紙
商品画像:Amazon.co.jp
PR Java基礎学習後の若手向け

スッキリわかるJava入門 実践編 第5版

中山 清喬 (著), 株式会社フレアリンク (監修)

基礎文法の次に必要な、現場寄りのJava知識を補う。

コレクション、ジェネリクス、ラムダ式、ストリームなど、業務コードで出会いやすい機能を入門編の次に学べます。

  • Java基礎の次に何を学ぶか迷っている
  • コレクションやStreamを整理したい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

まとめ

try-with-resourcesは、AutoCloseableを正常時と例外時の両方で自動closeする構文です。手動finallyより閉じ忘れを防ぎ、close例外をsuppressedとして残せます。

複数リソースのclose順、Streamの遅延評価、JDBCやSpringの管理契約も確認してください。取得した側が閉じる原則を基準にしつつ、フレームワークが所有するリソースを勝手に閉じないことが重要です。

-Java
-, , ,