エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

Java8 Stream APIの基本(8) - 終端操作3(close)

Stream APIの最終回として、使用頻度は少ないと思われるが、Stream#closeについて述べる。

Stream#close

StreamAutoCloseableを実装しているため、closeメソッドも存在する。 (正確には Stream のスーパーインターフェースのBaseStreamで実装されている)。 そのためStream型の変数をtry-with-resource句で宣言できる。

public static void main(String[] args) throws IOException {
  FileSystem fs = FileSystems.getDefault();
  BufferedReader br = Files.newBufferedReader(fs.getPath("sample.txt"));
  try (Stream<String> st = br.lines().onClose(() -> noCheckClose(br))){   
      st.forEach(System.out::println);
  }
}
// ラムダ式の内部で例外処理を書くと見づらくなるため別メソッドに切り出す
private static void noCheckClose(Reader r) {
    try {
        r.close();
    } catch (IOException ex) {
       throw new UncheckedIOException(ex);
    }
}

Stream#closeは中間操作のStream#onClose で設定した関数を順次実行するメソッドのため、onCloseを呼び出さなかった場合には何も行われない。
そのため、例えばBufferedReader#lines()Streamを取得した場合には、Stream#closeを呼んでも、生成元のBufferedReaderのリソースが自動的に解放されるわけではない。 もし、Stream#closeで生成元のリソースを解放したいなら、上記の例のように、Stream#onCloseで生成元リソースを解放する関数を指定する必要がある。
なお、onCloseは何度も呼び出し可能な中間操作であり、onCloseで設定した処理は、Stream#closeが実行されるときにまとめて実行される。

では、Stream#closeが有効なのはどのような場面だろうか。
Streamjavadocには、

ストリームはBaseStream.close()メソッドを持ち、AutoCloseableを実装していますが、ほとんどすべてのストリーム・インスタンスは、実際には使用後にクローズする必要はありません。一般に、クローズが必要になるのは、入出力チャネルをソースに持つストリーム(Files.lines(Path, Charset)から返されるものなど)だけです。

とある。

上記のjavadocで紹介されているFiles#linesは、パスを指定して当該パスにあるファイルを1行ずつの文字列のStreamを返すメソッドである。 このメソッドは内部でReaderを生成して、Stream#onCloseReadercloseを呼び出すように実装されている。
よって、Streamをtry-with-resource句で囲む事で、内部で生成したReaderが正しく解放される。

このFiles#linesのように、何らかの情報からIOに関するStreamを生成するような処理を作る場合に、Stream#close が有効となる。

ただし、Stream#onCloseを実装したとしても、呼び出し側で closeを実行するか、try-with-resource句を用いないと、解放は行われない。
そのため、Streamを明示的に解放する必要がある場合は、javadocなどによって注意を促す事が必要である。

また、Readerなどの入出力のリソースがすでにある状態でStreamを生成する場合には、以下のように直接リソースを解放する方が自然である。

public static void main(String[] args) throws IOException{
    FileSystem fs = FileSystems.getDefault();
    try (BufferedReader br = Files.newBufferedReader(fs.getPath("sample.txt"))){   
        br.lines().forEach(System.out::println);
    }
}

Stream API 全体の所感

今回まで、8回にわたってStreamの主なAPIを説明してきた。

Streamは、List#stream等のメソッドで既存のコレクション等から作成し、終端操作でStreamから戻るという流れになっている。 他の言語の同様な機能では、リストなどのデータ構造に直接filtermapなどの操作が可能なため、それと比較するとStreamは実行するメソッドが多く煩雑な印象がある。

これはJavaのコレクション、配列、IO等様々なデータの集合に統一的なAPIを導入するためだと思われる。 また、処理の遅延実行や並列処理といったStreamの性質をStream内部に封じ込むことで、他のコレクション等の機能と分離するという狙いもあるのだろう。

機能が分離されていることで、Streamの終端操作が終わるまでは、Stream APIを通じない限り、Streamで処理されているデータの書き換えなどの操作はできない。 この仕組みにすることで、Streamの内部で遅延実行や並列処理が行われていようと、Streamの外には影響を与えないのである。

Stream API は生成と終端処理があるため、一手間かかるのは確かだが、一方で様々なデータ集合を同一の方法で処理することを可能にしている。 また、並列処理への対応も容易である。 見通しが良く変更に強いコードを書くために、Stream APIは使用する価値があるといえる。

[前多 賢太郎]