Java8 Stream APIの基本(8) - 終端操作3(close)
Stream APIの最終回として、使用頻度は少ないと思われるが、Stream#close
について述べる。
Stream#close
Stream
はAutoCloseable
を実装しているため、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
が有効なのはどのような場面だろうか。
Stream
のjavadocには、
ストリームはBaseStream.close()メソッドを持ち、AutoCloseableを実装していますが、ほとんどすべてのストリーム・インスタンスは、実際には使用後にクローズする必要はありません。一般に、クローズが必要になるのは、入出力チャネルをソースに持つストリーム(Files.lines(Path, Charset)から返されるものなど)だけです。
とある。
上記のjavadocで紹介されているFiles#lines
は、パスを指定して当該パスにあるファイルを1行ずつの文字列のStream
を返すメソッドである。
このメソッドは内部でReader
を生成して、Stream#onClose
でReader
のclose
を呼び出すように実装されている。
よって、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
から戻るという流れになっている。
他の言語の同様な機能では、リストなどのデータ構造に直接filter
やmap
などの操作が可能なため、それと比較するとStream
は実行するメソッドが多く煩雑な印象がある。
これはJavaのコレクション、配列、IO等様々なデータの集合に統一的なAPIを導入するためだと思われる。
また、処理の遅延実行や並列処理といったStream
の性質をStream
内部に封じ込むことで、他のコレクション等の機能と分離するという狙いもあるのだろう。
機能が分離されていることで、Stream
の終端操作が終わるまでは、Stream APIを通じない限り、Stream
で処理されているデータの書き換えなどの操作はできない。
この仕組みにすることで、Stream
の内部で遅延実行や並列処理が行われていようと、Stream
の外には影響を与えないのである。
Stream API は生成と終端処理があるため、一手間かかるのは確かだが、一方で様々なデータ集合を同一の方法で処理することを可能にしている。 また、並列処理への対応も容易である。 見通しが良く変更に強いコードを書くために、Stream APIは使用する価値があるといえる。
[前多 賢太郎]