Java | Java 詳細・モダン文法:言語仕様詳細 – ntry-with-resources 拡張

Java Java
スポンサーリンク

まず「元祖」try-with-resources をおさらいする

try-with-resources は、
「使い終わったら必ず close() しなきゃいけないリソース(ファイル、ソケット、DB 接続など)」を
自動でクローズしてくれる構文です。

一番基本の形はこうです。

try (var reader = new BufferedReader(new FileReader("data.txt"))) {
    String line = reader.readLine();
    System.out.println(line);
} catch (IOException e) {
    e.printStackTrace();
}
Java

try (...) のカッコの中で宣言した変数は、
try ブロックを抜けるときに自動的に close() が呼ばれます。

ここまでは Java 7 からある「元祖」try-with-resources です。
拡張の話に行く前に、これが前提になっています。


旧仕様の制約:「try のカッコの中で宣言しないとダメ」

Java 7 時点のルール

Java 7 の頃の try-with-resources には、こんな制約がありました。

try のカッコの中には、“その場で宣言する”ローカル変数しか書けない」

つまり、こういうのは OK です。

try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
    // reader を使う
}
Java

でも、こういうのはダメでした。

BufferedReader reader = new BufferedReader(new FileReader("data.txt"));

// Java 7 ではコンパイルエラー
try (reader) {
    // reader を使う
}
Java

「すでに宣言済みの変数」を try のカッコにそのまま書くことはできなかったんです。

その結果、ちょっと不自然なコードが生まれます。

BufferedReader reader = new BufferedReader(new FileReader("data.txt"));

// もう一回同じ変数名で宣言し直すハメになる
try (BufferedReader r = reader) {
    // r を使う
}
Java

「reader を r に入れ直しているだけ」で、
やりたいことに対してノイズが多いですよね。


Java 9 の拡張:「すでにある変数」をそのまま使えるようになった

拡張の核心:カッコの中に「宣言済み変数」を書ける

Java 9 で入った try-with-resources の拡張は、
この不自然さを解消するためのものです。

Java 9 以降は、こう書けます。

BufferedReader reader = new BufferedReader(new FileReader("data.txt"));

try (reader) {  // ← ここに“宣言済みの変数”を書ける
    String line = reader.readLine();
    System.out.println(line);
}
Java

ポイントはここです。

try のカッコの中に、“新しく宣言”ではなく、“既にある変数名”を書けるようになった」

これが「try-with-resources 拡張」の中身です。
やっていることはシンプルですが、コードの自然さがかなり変わります。

ただし条件付き:「実質 final(effectively final)」であること

何でもかんでも既存変数を入れられるわけではなく、
その変数は 実質 final(effectively final) である必要があります。

「実質 final」とは、

final と書いてはいないけれど、
一度代入されたあと、二度と別の値を代入していない変数」

のことです。

例えば、これは OK です。

BufferedReader reader = new BufferedReader(new FileReader("data.txt"));
// ここで reader に再代入していない

try (reader) {  // OK
    ...
}
Java

でも、これは NG です。

BufferedReader reader = new BufferedReader(new FileReader("data1.txt"));
reader = new BufferedReader(new FileReader("data2.txt")); // 再代入している

try (reader) {  // コンパイルエラー(実質 final ではない)
    ...
}
Java

「リソースとして管理する変数は、途中で別のものに差し替えられないようにしておいてね」
というルールだと思ってください。


なぜ「実質 final」が条件なのかをちゃんと理解する

クローズされる対象をコンパイラが正しく追いかけるため

try-with-resources は、
「この変数が指しているリソースを、ブロックの最後に close() する」
というコードをコンパイラが自動生成します。

もし、その変数が途中で別のオブジェクトに差し替えられていたら、
「どのタイミングで、どのリソースを閉じるべきか」が曖昧になります。

例えば、こんなコードを想像してみてください。

BufferedReader reader = new BufferedReader(new FileReader("a.txt"));
reader = new BufferedReader(new FileReader("b.txt"));

try (reader) {
    ...
}
Java

ここで close() されるべきはどっちでしょう?

  • 最初の "a.txt" の reader?
  • 後から代入した "b.txt" の reader?

仕様としては「最後に代入されたもの」になるでしょうが、
そもそも「途中で差し替える」ような変数を
自動クローズの対象にするのは危険です。

だからこそ、
「一度代入したら二度と変えない(実質 final)」
という制約を課しているわけです。

設計的にも「リソースは変数を使い回さない」方が安全

実務的にも、
「リソースを表す変数に、別のリソースを再代入する」
という設計はあまり良くありません。

InputStream in = open(...);
in = open(...); // 上書きしてしまう
Java

こういうコードは、
「最初に開いたリソースを閉じ忘れる」
というバグの温床になります。

try-with-resources の拡張は、
「リソース変数は実質 final にしてね」
という形で、設計のベストプラクティスを言語仕様側から後押ししているとも言えます。


拡張によって書けるようになった「自然なコード」の例

例1:リソースを先に作ってから、あとで try に渡す

拡張前は、こう書くしかありませんでした。

BufferedReader reader = new BufferedReader(new FileReader("data.txt"));

try (BufferedReader r = reader) {
    String line = r.readLine();
    System.out.println(line);
}
Java

拡張後は、こう書けます。

BufferedReader reader = new BufferedReader(new FileReader("data.txt"));

try (reader) {
    String line = reader.readLine();
    System.out.println(line);
}
Java

「reader を使う」と決めたら、
ずっと reader という名前のまま使えるので、
読み手の頭の負荷が減ります。

例2:複数のリソースを組み合わせる

もちろん、複数リソースも書けます。

var in  = new FileInputStream("input.txt");
var out = new FileOutputStream("output.txt");

try (in; out) {
    // in から読み、out に書く
}
Java

ここでも、in / out は実質 final である必要があります。
途中で別のストリームを代入してはいけません。


「拡張された try-with-resources」をどう使い分けるか

その場で作るなら、従来の書き方でもいい

例えば、こういうコードは今でも自然です。

try (var reader = new BufferedReader(new FileReader("data.txt"))) {
    ...
}
Java

「リソースはこのブロックの中だけで使うし、
外から渡されるわけでもない」
という場合は、従来のスタイルで十分です。

外から渡されたリソースを「安全に閉じたい」ときに効く

拡張が特に効いてくるのは、
「外から渡されたリソースを、このメソッドの中で閉じたい」
というケースです。

void process(BufferedReader reader) {
    // Java 9 以降ならこう書ける
    try (reader) {
        ...
    } catch (IOException e) {
        ...
    }
}
Java

Java 7 だと、こう書くしかありませんでした。

void process(BufferedReader reader) {
    try (BufferedReader r = reader) {
        ...
    } catch (IOException e) {
        ...
    }
}
Java

「引数の readerr に入れ直す」という無駄な一手間が消え、
「このメソッドは reader を閉じる責任を持っている」
という意図が、コードからストレートに伝わるようになります。


まとめ:try-with-resources 拡張を自分の言葉で説明するなら

あなたの言葉で整理すると、こうなります。

「元々の try-with-resources は、
try (Resource r = new Resource()) { ... } のように、
try のカッコの中で“新しく宣言した変数”しか自動クローズの対象にできなかった。

Java 9 の拡張で、
try (r) のように“すでに宣言済みのローカル変数”も
自動クローズの対象にできるようになった。
ただし、その変数は“実質 final”(一度代入したら再代入していない)である必要がある。

これによって、
・外から渡されたリソースをそのまま try-with-resources に渡せる
・変数名を無理に変えたり、二重に宣言したりする必要がなくなる
・リソース変数を実質 final にすることで、設計としても安全になる。

つまり、拡張された try-with-resources は、
『リソースの寿命と責任範囲を、より自然なコードで表現できるようにしたもの』
と捉えるとわかりやすい。」

タイトルとURLをコピーしました