1. はじめに
Java でのファイル操作(それ以外でも!)は、常に外部リソースを扱うことを意味します。ファイルを開くと、オペレーティングシステムはあなたのプログラムに「ディスクリプタ」— ファイルの読み書きを可能にする特別な識別子 — を割り当てます。こうしたディスクリプタの数には制限があります。ファイルを閉じないでいると、プログラムが利用可能なリソースをあっという間に食いつぶし、"Too many open files" のような不可解なエラーを投げ始めます。
さらに、ファイルを閉じないと、他のプログラムからロックされたままになることがあります。例えば、ファイルを出力用に開いて閉じ忘れると、あなたも他の誰もそれを変更したり削除したりできません。ファイルシステムの世界における、まるで「人質を永久に取り続ける」状態です。
実例
FileInputStream fis = new FileInputStream("data.txt");
int b = fis.read();
// ... いろいろ処理して、そして fis.close() を呼び忘れる
メソッド close() を呼ばないと、ファイルはプログラム終了まで「ぶら下がった」ままになります。大規模アプリではリソースリークやアプリのクラッシュにつながることもあります。
2. 旧来のやり方: finally + close()
Java 7 以前の、ファイルを確実に閉じるための古典的なやり方は次のとおりでした:
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
// ファイルを読む
int b = fis.read();
// ...
} catch (IOException e) {
System.out.println("ファイル読み込み中のエラー: " + e.getMessage());
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
System.out.println("ファイルクローズ時のエラー: " + e.getMessage());
}
}
}
このアプローチの短所
- finally ブロックを書き忘れて、リソースリークを起こしやすい。
- とくに複数ストリームの場合に冗長なコードになりがち。
- クローズ時の例外も別途キャッチする必要がある。
- コードがかさばって読みづらくなる。
3. 現代的アプローチ: try-with-resources
幸い、Java 7 でこれらの問題をエレガントかつ自動で解決する構文 — try-with-resources — が導入されました。
見た目はこうなります
try (FileInputStream fis = new FileInputStream("data.txt")) {
int b = fis.read();
// ファイルを扱う
} catch (IOException e) {
System.out.println("ファイル操作中のエラー: " + e.getMessage());
}
// ここでは fis はすでに自動的にクローズされている!
主なポイント: try の後ろの丸括弧で宣言したすべてのリソースは、ブロック終了時に自動的に閉じられます — 途中で例外が発生しても同じです。finally は不要で、クローズ失敗のエラーを個別に捕捉する必要もありません — Java がすべてやってくれます。
try-with-resources で使えるクラスは?
AutoCloseable(または昔ながらの Closeable)を実装しているクラスなら何でも使えます。標準の入出力ストリームのほとんどが該当します: FileInputStream、FileOutputStream、BufferedReader、BufferedWriter、Scanner、PrintWriter など多数。
4. try-with-resources の構文: 詳細と例
単一リソース
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("エラー: " + e.getMessage());
}
// reader は自動的にクローズされる!
複数リソース
複数のリソースはセミコロン区切りで宣言できます:
try (
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))
) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
} catch (IOException e) {
System.out.println("コピー中のエラー: " + e.getMessage());
}
// 両方のストリームがクローズ済み!
クローズ順序: リソースは宣言とは逆順に閉じられます。最初に writer.close() が呼ばれ、その後に reader.close() が呼ばれます。あるストリームが別のストリームに依存している場合に重要です。
カスタムクラスでの使用
リソースを扱う独自クラスを書く場合は、AutoCloseable インターフェースを実装するだけです:
class MyResource implements AutoCloseable {
public void doSomething() {
System.out.println("リソースを操作中!");
}
@Override
public void close() {
System.out.println("リソースをクローズ!");
}
}
try (MyResource res = new MyResource()) {
res.doSomething();
}
// ブロックを抜けると次が出力される: "リソースをクローズ!"
5. どのように動くか: 図解
flowchart TD
A[try-with-resources でリソースを開く] --> B{try ブロックで例外が発生したか?}
B -- いいえ --> C[リソースは自動的にクローズされる]
B -- はい --> D[リソースは自動的にクローズされる]
D --> E[例外は次へ伝播される]
C --> F[プログラムは実行を続ける]
結論: エラーの有無にかかわらず、リソースは必ず閉じられます!
6. 例: コードを「今風」に書き換える
Before(旧スタイル):
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("input.txt"));
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("エラー: " + e.getMessage());
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
System.out.println("クローズ時のエラー: " + e.getMessage());
}
}
}
After(try-with-resources):
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("エラー: " + e.getMessage());
}
// もう finally は不要!
7. クローズ中にエラーが起きたらどうなる?
ときどき、リソースのクローズ自体が例外を送出することがあります(たとえばファイルが突然消えた場合など)。try-with-resources では、そうした例外は失われません。もし try ブロック内で既に例外が発生しており、さらにリソースのクローズ中に別の例外が起きた場合、その例外はメインの例外に「抑制された(suppressed)」例外として追加されます。これは Throwable.getSuppressed() メソッドで確認できます。
例
try (MyResource res = new MyResource()) {
throw new IOException("try ブロック内のエラー");
} catch (IOException e) {
System.out.println("メインのエラー: " + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("抑制された例外: " + suppressed.getMessage());
}
}
8. try-with-resources をサポートするクラスは?
簡単です: AutoCloseable インターフェースを実装しているクラスなら使えます。以下は標準クラスの一部です:
| クラス | 用途 |
|---|---|
|
ファイルからバイトを読み込む |
|
ファイルにバイトを書き込む |
|
テキストの読み書き |
|
ストリームのバッファリング |
|
整形付きでテキストを書き出す |
|
ファイル/コンソールからデータを読む |
|
シリアライズ/デシリアライズ |
|
ZIP アーカイブの操作 |
|
ネットワーク接続 |
サードパーティ製ライブラリを使う場合はドキュメントを確認しましょう。close() メソッドがあれば、たいてい try-with-resources をサポートしています。
9. ヒントと有用な補足
try の外で宣言可能(Java 9 以降): 既に宣言済みのリソースでも、final または「effectively final」であれば使用できます:
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try (reader) {
// ...
}
対象はファイルだけではありません: try-with-resources は、ネットワーク接続、データベース、close() メソッドを持つ任意のオブジェクトなど、あらゆるリソースに便利です。
例外を無視しない: try-with-resources を使っても、例外を捕捉・処理するのは重要です。万能薬ではなく、リークを防ぐための便利な仕組みに過ぎません。
try の中で手動でリソースを閉じない: 必要ありません — Java がやってくれます! close() を手動で呼んだ後に try ブロックが終わると、すでに閉じたリソースを再度閉じようとします。通常は安全ですが、混乱のもとになります。
10. try-with-resources でよくある間違い
誤り №1: try-with-resources をそもそも使っていない。 まだ finally { resource.close(); } と書いているなら、2011 年にいるか、この講義を読んでいないかのどちらかです! 現代的な構文を使いましょう。
誤り №2: リソースを try の外で宣言して中で使うだけ。 このコードではリソースは自動的に閉じられません:
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try {
// ... reader を使用
} finally {
// ここでクローズを忘れている!
}
誤り №3: try ブロック内で手動で close() を呼ぶ。 致命的ではありませんが冗長で、二重クローズにつながることがあります。Java に任せましょう。
誤り №4: IO 固有の例外を無視して、Exception だけを捕捉する。 FileNotFoundException や IOException など、具体的な例外を捕捉して、ユーザーに分かりやすいメッセージを提示しましょう。
誤り №5: 抑制された例外を処理しない。 リソースのクローズ中にエラーが発生すると、それは「抑制」されることがあります。エラー解析を行う場合は getSuppressed() も忘れずに確認しましょう。
GO TO FULL VERSION