技術と日常。

日々の気が付いたこと・気になったことを残しておきます。

[Java]Streamを閉じないとリソースリークするよ!っていう話(AutoClosable)

本記事は、JJUGにて登壇したものを、記事向けに加筆修正したものです。
その際の動画・スライドはこちらのページでご覧頂けます。 beppy.hatenablog.com

StreamのAPI仕様確認

StreamのAPI仕様を確認すると、AutoClosableがついています

AutoClosableがついているということは、以下のように、try-with-resources文にて、リソースとして宣言ができます。
(このケースでは、こう書き換えても何もメリットはないのですが……。)

Stream.of(1, 2, 3).forEach(System.out::println);
↓
try (Stream<Integer> stream = Stream.of(1, 2, 3)) {
    stream.forEach(System.out::println);
}

> Task :run
1
2
3

AutoClosableが必要となるケース

ではなぜこれが用意されているのかというと、 Readerや、DBのConnection等のリソースを、Stream内部で使用している場合に、使い終わった時にcloseができるようにするため です。
怠ると、リソースやコネクションのリークにつながる可能性があります。

java.nio.file.Files

list

public static Stream list(Path dir) throws IOException

ディレクトリ内のエントリを要素に持つ遅延設定Streamを返します。 リストは再帰的ではありません。
このメソッドは、 try-with-resources文または類似の制御構造内で使用して、ストリーム操作が完了した後にストリームのオープン・ディレクトリがすぐに閉じられるようにする必要があります。

lines

public static Stream lines(Path path, Charset cs) throws IOException

ファイル内のすべての行をStreamとして読み取ります。
返されるストリームは、Readerをカプセル化します。 ファイル・システム・リソースのタイムリな破棄が必要な場合は、try-with-resources構文を使用して、ストリーム操作の完了後にストリームのcloseメソッドが呼び出されるようにしてください。

jOOQ(ORM)

ResultQuery

Stream stream() throws DataAccessException

……
Clients should ensure the Stream is properly closed, e.g. in a try-with-resources statement:

try (Stream stream = query.stream()) {
// Do things with stream
}

close処理を付与したいなら

上記のようなtry-with-resources文で閉じてくれるStreamを自作したい場合、onClose()を使用することで作成することができます。

S onClose(Runnable closeHandler)

追加のクローズ・ハンドラを含む同等のストリームを返します。
クローズ・ハンドラはストリーム上でclose()メソッドが呼び出されたときに実行されますが、ハンドラの実行順序は追加された順番になります。
いずれかのクローズ・ハンドラから例外がスローされても、すべてのクローズ・ハンドラが実行されます。
……
これは中間操作です。

例えば、以下のようなコードを実行すると、try-with-resource文を抜けたときに、追加された順番でハンドラが実行されていることが確認できます。

Runnable first = () -> System.out.println("first called");
Runnable second = () -> System.out.println("second called");
try (Stream<Integer> stream
        = Stream.of(1, 2, 3).onClose(first).onClose(second)) {
    stream.forEach(System.out::println);
}

> Task :run
1
2
3
first called
second called

もっと詳しく知りたい場合、前述したFiles.lines()の実装が参考になりそうなので、併せてご確認下さい

最後に

恥ずかしながら、私はStreamが閉じれることを知らず、特にjOOQのは見逃す寸前で、危うくコネクションリークを起こすところでした。
改めて、ドキュメントやJavaDocはちゃんと読んで、理解してから使う必要があるなと、反省しました。

それでももし見落としたとしても、本知識があれば「このStreamは外部のリソースを使ってそうだけど大丈夫かな……?」と、注意するきっかけとなると思います。
本記事が、皆様のよいJava Lifeの助けになれば幸いです。

参考・出典

Files (Java SE 17 & JDK 17)

ResultQuery (jOOQ 3.17.4 API)

BaseStream (Java SE 17 & JDK 17)

Java8 Stream APIの基本(8) - 終端操作3(close) - エンタープライズギークス (Enterprise Geeks)