Stream APIの拡張(1) - 生成処理の拡張
Stream APIには、他の言語の同様の機能(すなわちHaskellのリスト([]
)やScalaのコレクション、C#のLINQなど)と比較すると、存在しない機能がいくつかあり、その中には有用なものがある。
以前の記事で紹介したzip
もその中の一つだ。
今回は、HaskellやScala、LINQなどがサポートしているtakeWhile
とdropWhile
について紹介し、Stream APIの生成処理を拡張することで、これらの機能を追加する方法を解説する。
10万以下の累乗数をすべて求めるコード
Stream APIを使用して、10万以下の累乗数*1をすべて取得するには、どうすればよいだろうか。
無限ストリームとfilter
を使用すればよいと考えるかもしれない。
List<Integer> less100000 = Stream.iterate(1, n -> n + 1) .map(n -> n * n) .filter(n -> n <= 100000) .collect(Collectors.toList());
残念ながら、このコードは無限ループに陥る。
iterate(1, n -> n + 1)
は1から始まる自然数なので、人の目から見ればある要素を境に10万以下の累乗数が見つからなくなるのは自明だろう。
しかし、filter
は全ての要素から条件に合致する要素を抽出するだけで、要素の繰り返しを停止するメカニズムを持っていない。このため、該当する要素がなくなったとしても全ての要素に対して処理を続けてしまい、無限ループになる。
limit
を使えば処理件数を制限できるが、具体的な数を指定する必要があるため、10万以下の累乗数の総数がわからない場合には適用できない。
無限ストリームをやめて、有限のリストや配列などを使う方法もあるが、拡張性や実行効率の点で適切な方法とは言えない。
結論として、現状のStream APIが提供する中間操作を組み合わせるだけでは、この問題をスマートに解決することは難しいと言える。
takeWhile
とは
takeWhile
とは、述語(要素T
を取り、boolean
を返す関数)を指定し、その述語が真となる間だけ要素を取得する機能である。 述語が偽となった時点で、以降の要素の取得は行わない(最初の要素に対する述語が偽となった場合には、何も取得しない)。
手続き型の処理で言えば、ループのbreak
を任意の条件で実現する仕組みに相当する。ちなみに、このtakeWhile
は、Scalaではコレクションのメソッドとして提供されており、他の多くの関数型言語でも標準関数として提供されている。
これをJava8で実現するにはどうすればよいだろうか。
以前書いたStream APIの基本(4)では、Stream
の要素の走査は内部的にSpliterator
を用いており、Spliterator
はIterator
から作成できることを解説した。この仕組みを利用することで、takeWhile
や後述するdropWhile
の性質を持ったStream
を作ることができる。
(ちなみに、Stream APIの基本(5)で紹介したzip
の実装例でもこの機能を利用している)。
takeWhile
の実装例
Stream
は、List#stream
などの生成メソッド内部で具象クラスを生成する仕組みになっているため、その具象クラスを拡張してtakeWhile
メソッドを定義する方法を使えない。
そこでここでは、ユーティリティとしてStreamUtil
クラスを用意し、takeWhile
の性質を内包したStream
を生成するcreateTakeWhile
メソッドを定義した。
public class StreamUtil { public static <T> Stream<T> createTakeWhile(Stream<T> source, Predicate<T> p) { Spliterator<T> splt = source.spliterator(); /* * characteristics(性質)は、 * Stream生成元のオブジェクト(コレクションや配列など)が持つ性質 * (Listなら順序あり、Setなら要素がユニークなど)であり、 * 新しく作成するSpliteratorに性質を引き継ぐため最初に取得する。 */ int characteristics = splt.characteristics(); Iterator<T> base = Spliterators.iterator(splt); Iterator<T> itr = new Iterator<T>() { T next; boolean end; // 述語が成立しなくなった時点でtrueとなる。 @Override public boolean hasNext() { // 元の要素がなくなるか、述語が成立しなくなるまで取得可能。 if (!base.hasNext() || end) { return false; } next = base.next(); if (p.test(next)) { return true; } else { end = true; return false; } } @Override public T next() { return next; } }; return StreamSupport.stream( Spliterators.spliteratorUnknownSize( itr,characteristics), false); } }
Stream
を生成するStreamUtil#createTakeWhile
メソッドが受け取る引数は2つある。1つは元となるStream
で、もう1つは要素取得の継続の是非を判定する述語関数だ。
実際にStream
の要素の走査が行われた場合には、元のStream
から取り出したIterator
と述語関数を使って、条件が成立している間だけ値を取得できるようにしている。
(Spliterator
の仕組みについては、Stream APIの基本(4)を参照のこと。)
StreamUtil#createTakeWhile
の使用例
10万以下の累乗数をすべて求めるコードを、上記のStreamUtil#createTakeWhile
使って実装すると次のようになる。
Stream<Integer> squareNums = Stream.iterate(1, n -> n + 1).map(n -> n * n); List<Integer> less100000= StreamUtil .createTakeWhile(squareNums, n -> n <= 100000) .collect(Collectors.toList()); System.out.println(less100000.size()); // 316個
dropWhile
とは
takeWhile
が述語が真の間だけ値を取得する処理であったのに対し、dropWhile
は 述語が真の間は要素を取得せず読み飛ばす機能である。条件が成立するまで処理を行わない場合に適用できる。
手続き型の処理でいえば、任意の条件に対するcontinue
に相当する。
こちらもStreamUtil#createTakeWhile
の実装同様、Iterator
を使って実現する。
dropWhile
の実装例
dropWhile
機能の実装例を以下に示す。
基本的な仕組みはStreamUtil#createTakeWhile
と同じである。
public class StreamUtil { // takeWhileの定義は割愛 public static <T> Stream<T> createDropWhile(Stream<T> source, Predicate<T> p) { Spliterator<T> splt = source.spliterator(); int characteristics = splt.characteristics(); Iterator<T> base = Spliterators.iterator(splt); Iterator<T> itr = new Iterator<T>() { T next; boolean start; @Override public boolean hasNext() { if (!base.hasNext()) { return false; } next = base.next(); // 述語が成立しなくなるまで、要素を読み飛ばす。 if (!start) { while(p.test(next) && base.hasNext()) { next = base.next(); } if (base.hasNext()) { start = true; } else { return false; } } return true; } @Override public T next() { return next; } }; return StreamSupport.stream( Spliterators.spliteratorUnknownSize( itr, characteristics), false); } }
StreamUtil#createDropWhile
の使用例
10万を超えた最初の累乗数を取得するコードは次のようになる。
Stream<Integer> squareNums = Stream.iterate(1, n -> n + 1).map(n -> n * n); Optional<Integer> over100000 = StreamUtil .createDropWhile(squareNums, n -> n < 100000) .findFirst(); over100000.ifPresent(System.out::println);
生成処理を拡張する方法の限界
今回の記事では、Stream
の生成処理に手を入れることで、Stream APIに機能を追加できることを紹介した。
ここで紹介したStreamUtil#createTakeWhile
やStreamUtil#createDropWhile
のような仕組みを用意することで、より高度な繰り返し処理をStream APIに追加できる。
一方で、上記の使用例のコードを見てもわかるように、Stream
インタフェースの中間操作として、メソッドチェーンでこれらのメソッドを連結することはできない。このため、このような機能拡張を多用すると、コードが見づらくなるデメリットがある。
Stream APIを拡張するもう一つの方法としては、委譲を用いる方法もあるが、これに関しては次回紹介する。
[前多 賢太郎]