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()を冪等にする;- クローズ後のオブジェクトの使用をブロックする。
さらに、コンストラクタ内にリソースをネストするのを避け、ブロックの外にリソースを返さないようにし、コードが実際に所有するリソースのみをクローズします。