try-with-resourcesとAutoCloseableのJava

1. 概要

ファイル、ソケット、データベース接続、決済ゲートウェイセッションなどは、使用後にクローズしなければならないリソースです。クローズを忘れると、アプリケーションはファイルディスクリプタをリーク、コネクションプールを枯渇させたり、不要に長くロックを保持したりします。

この記事では、リソースを自動的にクローズするためのtry-with-resourcesの使用方法を説明します。AutoCloseableを実装するカスタムクラスを書く方法、Javaがクローズ時に発生するexception(suppressed exception)を処理する方法、およびこの構文を使用する際の一般的なエラーについても学びます。

2. try-finallyの問題点

Java 7以前では、リソースをクローズする標準的な方法はfinallyブロックを使用することでした:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class LegacyOrderReader {

    public static String readFirstLine(String path) throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader(path));
        try {
            return reader.readLine();
        } finally {
            reader.close();
        }
    }
}

このコードは動作しますが、2つの問題があります:

  • 冗長である。 2つまたは3つのリソースがある場合、複数のtry-finallyブロックをネストする必要があります。各ブロックでもnullチェックが必要です。
  • 元のexceptionが隠れる。 readLine()がIOExceptionをスロー、その後close()もexceptionをスロー場合、finallyからのexceptionが元のexceptionを置き換えます。実際のエラーに関する情報が失われます。

try-with-resourcesはこれら両方の問題を解決します。

3. try-with-resourcesの構文

キーワードtryの直後の括弧内でリソースを宣言します:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class OrderReader {

    public static String readFirstLine(String path) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
            return reader.readLine();
        }
    }
}

tryブロックが終了したとき、Javaは自動的にreader.close()を呼び出します。これはブロックが正常に実行される場合、returnに遭遇する場合、およびexceptionが発生する場合に当てはまります。finallyを書く必要はありません。

唯一の条件は、変数の型がjava.lang.AutoCloseableを実装している必要があることです。JDKのほとんどのI/Oクラスはこの条件を満たします。例えば、InputStream、Reader、Connection、Statement、ResultSet、Scannerなどです。

3.1. 複数リソースとクローズ順序

複数のリソースを宣言でき、セミコロンで区切ります。Javaはそれらを宣言順序の逆順でクローズします:

public class ClosingOrderDemo {

    record NamedResource(String name) implements AutoCloseable {
        NamedResource {
            System.out.println("開く " + name);
        }

        @Override
        public void close() {
            System.out.println("閉じる " + name);
        }
    }

    public static void main(String[] args) {
        try (var connection = new NamedResource("connection");
             var statement = new NamedResource("statement");
             var resultSet = new NamedResource("resultSet")) {
            System.out.println("顧客データを読む");
        }
    }
}

結果:

開く connection
開く statement
開く resultSet
顧客データを読む
閉じる resultSet
閉じる statement
閉じる connection

この順序は理にかなっています。後で宣言されたリソースは通常、前に宣言されたリソースに依存します。例えば、statementはconnectionに依存するため、先にクローズする必要があります。

覚えておくべき点があります。2番目のリソースがコンストラクタで直接exceptionをスロー場合、Javaは最初のリソースが正常に開かれていればそれをクローズします。まだ作成されていないリソースにはclose()が呼び出されません。

3.2. 既存の変数を使用する(Java 9以降)

Java 9から、try内に外部で宣言された変数を含めることができます。その変数がfinalまたはeffectively finalであれば問題ありません:

import java.util.Scanner;

public class CustomerInput {

    public static void main(String[] args) {
        Scanner scanner = new Scanner("Nguyen Van A\n0901234567");
        try (scanner) {
            System.out.println("名前:" + scanner.nextLine());
            System.out.println("電話番号:" + scanner.nextLine());
        }
    }
}

この方法は、リソースがパラメータから渡されるか、複雑なfactoryによって作成される場合に便利です。

3.3. catchとfinallyはクローズ後に実行される

try-with-resourcesはcatchとfinallyの追加を許可します。しかし、両方ともリソースがクローズされた後に実行されます。したがって、catch内でそのリソースを再度使用することはできず、リソース変数もスコープ外です。

4. カスタムAutoCloseableクラスの実装

AutoCloseableは1つのメソッドだけを持ちます:

void close() throws Exception;

決済ゲートウェイのセッションがあり、各注文の後にこのセッションを解放する必要があるとします:

public class PaymentGatewaySession implements AutoCloseable {

    private final String orderId;
    private boolean closed;

    public PaymentGatewaySession(String orderId) {
        this.orderId = orderId;
        System.out.println("決済セッションを開く " + orderId);
    }

    public void charge(long amount) {
        if (closed) {
            throw new IllegalStateException("セッションが閉じられています");
        }
        if (amount <= 0) {
            throw new IllegalArgumentException("無効な金額:" + amount);
        }
        System.out.println("引き落とし " + amount + " VND 注文 " + orderId);
    }

    public boolean isClosed() {
        return closed;
    }

    @Override
    public void close() {
        if (closed) {
            return;
        }
        closed = true;
        System.out.println("決済セッションをクローズ " + orderId);
    }
}

このクラスで注意する3つのポイントがあります:

  • close()はthrows Exceptionを宣言しません。オーバーライドされたメソッドは、checkした例外のリストを絞ることが許可されています。これにより、クラスのユーザーはcatch (Exception e)を強制されません。
  • close()は冪等性があります:複数回呼び出してもエラーが発生せず、2回解放されません。
  • charge()はclosed状態をチェックして、クローズ後のリソース使用をブロックします。

使用方法:

public class CheckoutService {

    public static void main(String[] args) {
        try (var session = new PaymentGatewaySession("ORD-1001")) {
            session.charge(250_000);
        }
    }
}
決済セッションを開く ORD-1001
引き落とし 250000 VND 注文 ORD-1001
決済セッションをクローズ ORD-1001

4.1. AutoCloseableか、Closeableか?

java.io.Closeableは Java 5 から存在し、AutoCloseableを継承します。2つのinterfaceは以下の点で異なります:

AutoCloseable Closeable
close()のException Exception IOException
冪等性 推奨 コントラクトにより必須
目的 あらゆるリソース I/Oリソース

リソースがI/Oに関連している場合は、Closeableを使用します。その他の場合、ロック、セッション、トランザクションなど、AutoCloseableを使用し、throwsをできるだけ絞ります。

5. Suppressed exception

セクション2に戻ります:tryブロックがexceptionをスロー、その後close()もexceptionをスロー場合。try-with-resourcesでは、tryブロックからのexceptionは主要なexceptionとして保持されます。close()からのexceptionはsuppressedとして主要なexceptionに添付されます:

public class SuppressedDemo {

    static class FlakyConnection implements AutoCloseable {
        @Override
        public void close() {
            throw new IllegalStateException("接続をクローズするときのエラー");
        }
    }

    public static void main(String[] args) {
        try (var connection = new FlakyConnection()) {
            throw new RuntimeException("注文を保存するときのエラー");
        } catch (RuntimeException e) {
            System.out.println("主要:" + e.getMessage());
            for (Throwable suppressed : e.getSuppressed()) {
                System.out.println("Suppressed:" + suppressed.getMessage());
            }
        }
    }
}

結果:

主要:注文を保存するときのエラー
Suppressed:接続をクローズするときのエラー

両方の情報を保持します。e.printStackTrace()またはloggerでスタックトレースを出力すると、Suppressed:部分も表示されます。おかげで、本番環境でのデバッグがはるかに簡単になります。

tryブロックが正常に実行され、close()だけがexceptionをスロー場合、close()からのexceptionが主要なexceptionになります。

6. リソースクローズ動作のテスト

テストを書くには、JUnit 5とAssertJを追加する必要があります:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.11.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.26.3</version>
    <scope>test</scope>
</dependency>

以下のテストは2つの主要な動作をチェックします:エラーが発生した場合でもリソースは常にクローズされ、close()からのexceptionはsuppressedとして主要なexceptionに添付されます。

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class PaymentGatewaySessionTest {

    @Test
    void shouldCloseSessionEvenWhenChargeFails() {
        var session = new PaymentGatewaySession("ORD-2002");

        assertThatThrownBy(() -> {
            try (session) {
                session.charge(-5);
            }
        }).isInstanceOf(IllegalArgumentException.class)
          .hasMessageContaining("-5");

        assertThat(session.isClosed()).isTrue();
    }

    @Test
    void shouldAttachCloseFailureAsSuppressed() {
        AutoCloseable failingResource = () -> {
            throw new IllegalStateException("close failed");
        };

        assertThatThrownBy(() -> {
            try (failingResource) {
                throw new RuntimeException("business failed");
            }
        }).hasMessage("business failed")
          .satisfies(e -> assertThat(e.getSuppressed())
                  .singleElement()
                  .extracting(Throwable::getMessage)
                  .isEqualTo("close failed"));
    }

    @Test
    void closeShouldBeIdempotent() {
        var session = new PaymentGatewaySession("ORD-3003");
        session.close();
        session.close();

        assertThat(session.isClosed()).isTrue();
        assertThatThrownBy(() -> session.charge(100))
                .isInstanceOf(IllegalStateException.class);
    }
}

2番目のテストはlambdaを使用して高速にAutoCloseableを作成します。AutoCloseableはabstract methodを1つだけ持つため、functional interfaceのように使用できます。

7. よくあるエラー

7.1. コンストラクタ内にリソースをネストする

try (var in = new ObjectInputStream(new FileInputStream("orders.bin"))) {
    // ...
}

ObjectInputStreamのコンストラクタはstreamのヘッダを読み込み、IOExceptionをスロー可能です。この場合、変数inがまだ割り当てられていません。内部のFileInputStreamは開かれていますが、決してクローズされません。安全な方法は、各リソースを個別に宣言することです:

try (var file = new FileInputStream("orders.bin");
     var in = new ObjectInputStream(file)) {
    // ...
}

7.2. ブロックから外にリソースを返す

public static Stream<String> loadOrderLines(Path path) throws IOException {
    try (Stream<String> lines = Files.lines(path)) {
        return lines; // エラー:呼び出し元がstreamを受け取るときにstreamが既にクローズされています
    }
}

呼び出し元はstreamを使用しようとするとすぐにIllegalStateExceptionを受け取ります。2つの修正方法があります。最初の方法はtry内ですべてのデータを処理することです。例えばreturn lines.toList();。2番目の方法はstreamを返して、呼び出し元がそれをtry-with-resourcesでラップするようにします。

注意: I/Oリソースに関連したStreamだけをクローズする必要があります。例えばFiles.lines、Files.list、Files.walk。List.stream()から作成されたstreamはクローズする必要がありません。

7.3. close()内でInterruptedExceptionをスロー

AutoCloseableのJavadocは、close()からInterruptedExceptionをスロー しないことをお勧めします。理由は、このexceptionはsuppressされる可能性があり、threadのinterrupt状態が失われる可能性があるからです。close()内で待機する必要がある場合は、exceptionをキャッチしてからThread.currentThread().interrupt()を呼び出してください。

7.4. nullリソース

try (var conn = findConnectionOrNull())を宣言することは有効です。値がnullの場合、Javaは単にclose()の呼び出しをスキップします。ただし、これは通常、設計が不十分な兆候です。Optionalを使用するか、factoryからexceptionをスロー するべきです。

7.5. いつ使用してはいけないか

AutoCloseableを実装するすべてのオブジェクトがmethod内で直ちにクローズされるべきではありません。例えば、Spring Bootでは、DataSourceまたは共有HttpClientはapplication contextと一緒に生きるbeanです。Springはcontextがシャットダウンするときそれらをクローズします。共有beanをtry-with-resourcesでラップすると、他のコンポーネントがまだ使用している場合にそれをクローズしてしまいます。

try-with-resourcesは、コードが作成して所有しているリソースのみに使用します。

7.6. ヒント:Java 21のExecutorService

Java 19から、ExecutorServiceはAutoCloseableを実装します。メソッドclose()はすべてのtaskが完了するのを待ちます。これはvirtual threadを使用する場合に非常に便利です:

import java.util.concurrent.Executors;

public class InvoiceBatch {

    public static void main(String[] args) {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 1; i <= 3; i++) {
                int invoiceId = i;
                executor.submit(() -> System.out.println("Invoice #" + invoiceId + "を送信"));
            }
        }
        System.out.println("すべての請求書を送信しました");
    }
}

行「すべての請求書を送信しました」は常に最後に印刷されます。close()は3つのtaskすべてが完了するまでブロック してくるからです。3つの請求書間の順序は不定です。

8. 結論

try-with-resourcesはtry-finallyと比べてコードを短くし、より安全にします。リソースは常にクローズされ、開かれた順序と逆の順序でクローズされます。元のexceptionは、suppressed exceptionメカニズムのおかげで隠れません。

AutoCloseableクラスを自分で書く場合、以下のことを心がけてください:

  • close()のthrowsを絞る;
  • close()を冪等にする;
  • クローズ後のオブジェクトの使用をブロックする。

さらに、コンストラクタ内にリソースをネストするのを避け、ブロックの外にリソースを返さないようにし、コードが実際に所有するリソースのみをクローズします。

javatry-with-resourcesautocloseableexceptionbest-practices