try-with-resources

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

try-with-resources とは

AutoCloseable なリソースを安全に扱う try-with-resources 構文を学び、リソースの自動クローズを体験しよう。

リソース解放という、永遠の悩み

Java でファイル、ネットワーク接続、データベース接続などを扱うとき、必ず付いて回るのが「使い終わったら閉じる」という後始末の作業です。FileInputStream を開いたなら close() を呼ばないと OS のファイルハンドルが残ったままになり、最悪の場合「Too many open files でアプリが停止」という障害につながります。ConnectionSocket も同じで、閉じ忘れは時限爆弾です。

「メモリは GC (ガベージコレクタ) が片付けてくれるから安心」と思いがちですが、GC が回収するのはあくまで Java の ヒープ の中だけです。OS の世界にある「ファイルハンドル」「ソケット」「DB コネクション」などは、Java 側で明示的に close() を呼ばないと解放されません。これが Java プログラマがリソース管理を意識しないといけない最大の理由です。

そこで Java 7 で導入されたのが、本章の主役 try-with-resources です。これを使うと、try の宣言部にリソースを書くだけで、ブロックを抜けるときに自動で close() が呼ばれます。finally で泥臭く close() を書いていた時代から、ぐっと安全で読みやすいコードになります。

try-with-resources の基本構文

まず構文を見てみましょう。try の直後の () の中にリソースを宣言するのがポイントです。

Java

try (Resource r = new Resource()) { r.use(); }

try のあとに ( ... ) が付くこの記法が、try-with-resources の特徴です。() の中で宣言した変数 (r) は、try ブロックの中だけで有効なローカル変数になります。そして try ブロックを正常に抜けたときでも、例外で抜けたときでも、r.close()暗黙的に呼び出される ように Java コンパイラがコードを書き換えてくれます。

つまり開発者は、リソースを「new する」ところと「use() する」ところだけを書けば、後片付けは Java に任せられる、というわけです。finally ブロックを書く必要はありません。

旧来の try-finally との比較

try-with-resources がなかった時代、つまり Java 6 以前は、次のような重たいコードを書いていました。Before / After で並べてみます。

Java

// Before: Java 6 までの try-finally スタイル Resource r = null; try { r = new Resource(); r.use(); } finally { if (r != null) { r.close(); } }

Java

// After: Java 7 以降の try-with-resources try (Resource r = new Resource()) { r.use(); }

行数だけでなく、null チェックや finally ブロックといった「本筋ではないコード」が消え、書き手の意図が一目で伝わるようになりました。さらに重要なのは、旧スタイルだと r.close() 自体が例外を投げた場合に元の例外が握りつぶされてしまう、という落とし穴があったのですが、try-with-resources では close() の例外が Throwable.addSuppressed() で抑制例外として記録されるので、両方の情報が残ります。

旧スタイルで close()try の中に書くと、use() が例外を投げたときに close() が呼ばれない、という別のバグを生みます。finally に書けば呼ばれますが、今度は例外が握りつぶされる、という地雷だらけの世界でした。try-with-resources は、この厄介な問題を文法レベルで解決してくれる仕組みです。

AutoCloseable インターフェース

try-with-resources() の中に書けるのは、何でも良いわけではありません。java.lang.AutoCloseable インターフェースを実装したクラスだけが置けます。AutoCloseable の中身は、たった 1 つのメソッドです。

Java

public interface AutoCloseable { void close() throws Exception; }

標準ライブラリの FileInputStream BufferedReader Connection Scanner などは、すべてこの AutoCloseable (もしくはサブインターフェースの java.io.Closeable) を実装済みです。だから上記のクラスを try-with-resources() に書いておけば、ブロックを抜けるときに自動で close() が呼ばれる、という仕組みになっています。

自作のクラスでリソース管理をしたい場合は、implements AutoCloseable と書いて close() メソッドを実装するだけで OK です。今回の課題でもまさにこれを書きます。

diagram (will load when visible)

上の図のとおり、正常終了でも例外発生時でも、必ず close() が呼ばれるところがポイントです。Java コンパイラが裏側で finally { r.close(); } 相当のコードを自動生成してくれている、と理解しておくとイメージしやすいでしょう。

複数リソースの宣言

try-with-resources() の中には、セミコロン ; で区切って 複数のリソース を宣言できます。たとえば入力と出力を同時に開きたいときは次のとおりです。

Java

try ( BufferedReader in = new BufferedReader(new FileReader("in.txt")); BufferedWriter out = new BufferedWriter(new FileWriter("out.txt")) ) { String line; while ((line = in.readLine()) != null) { out.write(line); out.newLine(); } }

このとき覚えておきたいのが、close()宣言と逆順 で呼ばれる、ということです。上の例では out を先に閉じてから in を閉じます。これは、ふつう「後から開いたものは先に閉じる」(LIFO) のがリソース管理の鉄則なので、Java もそれに従うように設計されています。たとえば Connection の上に PreparedStatement を載せた場合、当然先に PreparedStatement を閉じてから Connection を閉じたいわけです。

ちなみに Java 9 以降では、すでに宣言済みで final (もしくは事実上 final) な変数を try-with-resources() に直接書くこともできます。たとえば try (resource) { ... } のように。とはいえまずは「try (Type name = expr) の形」を覚えておけば十分です。

今回の課題で作る Resource クラス

今回の課題では、ファイル IO のような重い仕組みを使わず、ログ文字列で「ライフサイクル」を観察できる軽量な Resource クラスを自分で書きます。Solution クラスの中に static inner class として定義するのがポイントです。

動きは次のとおりです。

  • コンストラクタが呼ばれたら、StringBuilder のログに "opened" を追記
  • use() を呼ばれたら、ログに "-used" を追記
  • close() が自動で呼ばれたら、ログに "-closed" を追記
  • 最後にログ文字列 "opened-used-closed"return する

この一連の流れを try-with-resources で書くと、たった 3 行で「開いて、使って、閉じる」が表現できます。close() を一切書いていないのに、-closed がちゃんとログに入るのを確かめましょう。

diagram (will load when visible)

よくある間違い

try-with-resources で初学者がつまずく代表的なポイントを 3 つまとめます。書き始める前にざっと目を通しておくと、後でデバッグに時間を取られずに済みます。

  • implements AutoCloseable を忘れるtry (Resource r = ...)Resource クラスが AutoCloseable を実装していないと、required type that implements AutoCloseable というコンパイルエラーになります。class Resource implements AutoCloseable と必ず書きましょう
  • close() の中で重い処理や例外を派手に投げるclose() は「後始末」のメソッドです。ここでネットワーク通信をしたり、独自の例外を派手に投げたりすると、デバッグが非常に難しくなります。close() は冪等 (idempotent) に、つまり何回呼んでも安全になるよう書くのが鉄則です
  • try() の中で複数リソースを書く順番を間違えるclose() は宣言と逆順で呼ばれるので、依存関係 (たとえば ConnectionStatementResultSet) がある場合は、依存される側を先に書き、依存する側を後に書きます。Connection Statement ResultSet の順に宣言すれば、逆順で ResultSetStatementConnection の順に閉じられる、ということです

もう 1 つよくあるのが、「try-with-resources を使えば例外処理は不要」と勘違いするケースです。try-with-resources がやってくれるのは close() の自動呼び出しだけで、use() の中で起きた例外を握りつぶしてくれるわけではありません。例外を握りたいなら、別途 catch ブロックを書く必要があります。

Java

try (Resource r = new Resource()) { r.use(); } catch (Exception e) { // ここで例外をハンドリング }

このように try-with-resourcescatch は併用できます。finally を書かなくても close() は呼ばれる、というのが新しい時代の Java の姿です。

やってみよう

それでは課題に挑戦してみましょう。次の手順で進めます。

  1. Solution クラスの中に static class Resource implements AutoCloseable を定義する
  2. Resource のフィールドに StringBuilder log を持たせ、コンストラクタで log.append("opened") する
  3. void use() メソッドで log.append("-used") する
  4. public void close() メソッドで log.append("-closed") する
  5. Solution.dummyClose() の中で StringBuilder を作り、try (Resource r = new Resource(log)) { r.use(); } と書き、最後に return log.toString(); する

テストは戻り値が "opened-used-closed" ちょうどになっているかを比較します。ハイフンの位置 (1 つ目だけ「opened」のように - が付かない) を間違えやすいので、最初の append だけ "opened"、後の 2 つは "-used" "-closed" と先頭にハイフンを付けるのがポイントです。

慣れてきたら、Resource を 2 つ宣言してみて、close() がどちらから呼ばれるかを試してみましょう。try (Resource a = new Resource(log); Resource b = new Resource(log)) のように書くと、ab の順に openba の順に close されるのが観察できます。実際の IODB 接続でも、この性質を活かして安全にリソースを解放するのが Java の流儀です。

よくある質問

Q. 中級の内容は実務でどれくらい使いますか?

A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。

Q. Stream と for ループはどっちで書くべき?

A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。

Q. 次のステップでは何を学ぶべきですか?

A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。

次のレッスン

次は finally ブロック で、AutoCloseable なリソースを安全に扱う try-with-resources 構文を学び、リソースの自動クローズを体験しよう を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. try-with-resources の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. try-with-resources とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. Solution の中に static class Resource implements AutoCloseable を定義し、close() メソッドを実装すること
  2. Resource のコンストラクタで "opened"use()"-used"close()"-closed"StringBuilderappend すること
  3. try-with-resources 構文 (try (Resource r = new Resource(log)) { ... }) を使い、finally で明示的に close() を呼ばないこと

入出力例

test-cases.txt

dummyClose()"opened-used-closed"

ヒント

main.java
main.java
学習モード

メモ

try-with-resources

⌘S で保存