Java : try-with-resources文でリソースを自動的に解放
try-with-resources文は、リソースの解放漏れを防ぐ非常に強力な文法です。
本記事では、そんなtry-with-resources文を解説していきます。
コード例
さっそくコード例を見てみましょう。
例としてwriteToFileメソッドを用意しました。
指定したファイルに123という数値を書き込みます。
public void writeToFile(Path path) throws IOException {
final var os = new FileOutputStream(path.toFile());
os.write(123);
os.close();
}
実はこのメソッドには問題があります。
わかりますでしょうか?
public void writeToFile(Path path) throws IOException {
final var os = new FileOutputStream(path.toFile());
os.write(123); // ここでIOExceptionが発生すると…
os.close(); // ★closeが呼び出されません。
}
書き込み時にIOExceptionが発生するとFileOutputStreamのcloseが呼びされず、ファイルが開きっぱなしになってしまいます。
これをリソースリークといいます。
補足
- 本来はFileOutputStreamを使うよりFiles.newOutputStreamを使うことをおすすめします。
今回はリソースリークの問題が確認しやすいFileOutputStreamを使用しました。
try-with-resources文
公式のJava言語仕様ではChapter 14に説明があります。
それでは先ほどのコードを改善してみましょう。
リソース解放したい変数をtry(...)の部分で宣言するだけです。
public void writeToFile(Path path) throws IOException {
try (final var os = new FileOutputStream(path.toFile())) {
os.write(123); // ここでIOExceptionが発生しても…
}
// ★スコープを抜けたときに自動でcloseを呼び出してくれます。
}
os.closeをコード上で呼び出す必要はなくなります。
さらにtryのスコープを抜けたら自動でcloseしてくれるようになります。
同様のことをtry-with-resources文を使わないで実現するには、以下のようになります。
public void writeToFile(Path path) throws IOException {
final var os = new FileOutputStream(path.toFile());
try {
os.write(123);
} finally {
os.close();
}
}
AutoCloseableインタフェース
try-with-resources文で自動解放できるのは、AutoCloseableインタフェースを実装しているクラスだけです。
標準APIには、さまざまなクラスやインタフェースがAutoCloseableを実装しています。
それらのクラスは基本的にはcloseが必要です。
積極的にtry-with-resources文を使いリソースの解放漏れがないように注意しましょう。
リソースリークは難解なバグを発生させる
リソースリーク…実際にはどのような問題が起こるのでしょうか?
先ほどの問題のあるコード例に戻りましょう。
public void writeToFile(Path path) throws IOException {
final var os = new FileOutputStream(path.toFile());
os.write(123); // ここでIOExceptionが発生すると…
os.close(); // ★closeが呼び出されません。
}
書き込み時にIOExceptionが発生することは十分ありえます。
例えば書き込み先のストレージ容量が足りなくなった場合です。
その後、そのファイルを操作しないのであれば、プログラムは問題なく動いているように見えるかもしれません。
しかしそれは間違いです。
そのファイルを削除や移動しようとすると例外が発生します。
try {
// writeでIOExceptionが発生したと仮定します。
writeToFile_ng(path);
} catch (IOException e) {
// 書き込みに失敗したのでファイルを削除したい、とします。
// "FileSystemException: R:\java-work\test.data: プロセスはファイルにアクセスできません。別のプロセスが使用中です。"
// が発生します。
Files.delete(path);
}
リソースリークでやっかいなのは、リークした瞬間には問題が発生せず、後になってから問題が発生することです。
上記の例では、リークが発生した場所と、問題(FileSystemException)が発生した場所が比較的近いので、問題解析は楽かもしれません。
ただ、問題の発生場所が離れていることもよくあります。
さらに、正常系では問題は発生せず、IOExceptionが発生するなど特定の条件でしか発生しないことも多いです。(再現性が低い)
これらの要素が重なり…問題解析は難解になることが多いです。
そのためAutoCloseableを実装しているクラスの扱いには常に注意し、try-with-resources文で解放漏れを予防するのが大事になります。
閑話
-
リソースリークで有名なのはメモリリークです。
少し古めのCやC++では苦しめられた方もいらっしゃるのではないでしょうか。
(最近はスマートポインタが導入されて、そのあたりはだいぶ改善したようです) -
Javaではガベージコレクションという機能によりメモリリソースは(基本的に)自動で解放してくれます。
そのためメモリリークで苦しむことはほぼないです。Javaの良いところですね。
(循環参照など、場合によってはJavaでもメモリリークすることはあります)
独自に定義したクラスをtry-with-resources文で使う
とはいえ、いつもいつもメソッド内の閉じたスコープ内でリソースを使うとは限りません。
try-with-resources文が使えない場面もあるでしょう。
// メソッド内の閉じたスコープでtry-with-resources文を使うのが理想です。
public void writeToFile(Path path) throws IOException {
try (final var os = new FileOutputStream(path.toFile())) {
os.write(123);
}
}
例えば、頻繁にファイルアクセスがあり、毎回ファイルのオープン・クローズを繰り返すとパフォーマンスに影響するとき、などでしょうか。
ちょっと実用的なクラスではないですが、以下のようなCountWriterクラスがあるとします。
// writeCountを呼び出すたびに、1, 2, 3, 4...とファイルに書き込むクラス。
// 最後にcloseでリソースを解放します。
public class CountWriter {
private final OutputStream os;
private byte count;
public CountWriter(Path path) throws IOException {
this.os = Files.newOutputStream(path);
}
public void writeCount() throws IOException {
count++;
os.write(count);
}
public void close() throws IOException {
os.close();
}
}
CountWriterを使う例です。
final var countWriter = new CountWriter(path);
countWriter.writeCount();
countWriter.writeCount();
countWriter.writeCount();
System.out.println(Arrays.toString(Files.readAllBytes(path))); // [1, 2, 3]
countWriter.close();
これでは冒頭のコード例と同じで、リソースリークの可能性がありますね。
writeCountで例外が発生したときです。
このようなケースでは、リソースを管理しているクラスにAutoCloseableを実装することを検討してみましょう。
今回の例ではCountWriterにAutoCloseableを実装します。
public class CountWriter implements AutoCloseable {
private final OutputStream os;
private byte count;
public CountWriter(Path path) throws IOException {
this.os = Files.newOutputStream(path);
}
public void writeCount() throws IOException {
count++;
os.write(count);
}
@Override
public void close() throws IOException {
os.close();
}
}
try (final var countWriter = new CountWriter(path)) {
countWriter.writeCount();
countWriter.writeCount();
countWriter.writeCount();
System.out.println(Arrays.toString(Files.readAllBytes(path))); // [1, 2, 3]
}
これで、CountWriterクラスもtry-with-resources文で使えるようになりました。
このように、なるべくtry-with-resources文で使えるように改善できないか検討してみることも大事です。
まとめ
- 初めて使うクラスやインタフェースは、API仕様でAutoCloseableを実装しているかどうか確認しましょう。
- 実装している場合は、使い終わったら必ずcloseを呼び出すようにしましょう。
- できる限りtry-with-resources文を使いましょう。
関連記事
- if文の基本
- while文の基本
- for文の基本
- プリミティブ型 (基本データ型)
- リテラルの表記方法いろいろ
- クラスの必要最低限を学ぶ
- インタフェースの default メソッドとは
- インタフェースの static メソッドの使いどころ
- var (型推論) のガイドライン
- アクセス修飾子の基本
- 配列 (Array) の使い方
- 拡張for文 (for-eachループ文)
- switch文ではなくswitch式を使おう
- テキストブロックの基本
- 列挙型 (enum) の基本
- ラムダ式の基本
- レコードクラスの基本 (Record Class)
- メソッド参照の基本
- シールクラスの基本(Sealed Class)
- 無名変数の使い方
- API 使用例